<?php

namespace App\Filament\Resources\InvoiceResource\Pages;

use App\Exceptions\InsufficientStockException;
use App\Filament\Resources\InvoiceResource;
use App\Http\Controllers\Concerns\AppliesCheckoutCredit;
use App\Models\Catalog\Product;
use App\Models\Sales\Invoice;
use App\Services\CustomerCreditService;
use App\Services\InvoiceService;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use RuntimeException;

/**
 * Ported from InvoiceController::checkoutComplete() — same lines/splits
 * shaping, same credit resolution, same single transaction wrapping invoice
 * creation + credit-note redemption, same exception handling.
 *
 * Uses the source's own AppliesCheckoutCredit trait (copied verbatim into
 * app/Http/Controllers/Concerns/) so POS, Sales Order and Invoice checkout all
 * still share one implementation, exactly as they do in the source.
 */
class CreateInvoice extends CreateRecord
{
    use AppliesCheckoutCredit;

    protected static string $resource = InvoiceResource::class;

    protected function handleRecordCreation(array $data): Invoice
    {
        InvoiceResource::assertBranchAccessible((int) $data['branch_id']);

        $customerId = (int) $data['customer_id'];
        $discountInput = (float) ($data['discount'] ?? 0);

        // The field is hidden without the permission, but a crafted payload
        // could still submit one — rejected outright, same as the legacy
        // invoice form. Only the manually-entered discount is gated; credit
        // notes applied at checkout are a separate, already-covered feature.
        if ($discountInput > 0 && ! Auth::user()?->hasPermissionTo('isms_invoices.discount')) {
            throw ValidationException::withMessages([
                'data.discount' => 'You do not have permission to apply a discount to an invoice.',
            ]);
        }

        // Product names are resolved server-side from the submitted ids, never
        // taken from the client — matching the source controller.
        $productNames = Product::whereIn('id', collect($data['items'])->pluck('product_id'))
            ->pluck('name', 'id');

        $lines = array_map(fn (array $item): array => [
            'product_id' => (int) $item['product_id'],
            'product_name' => $productNames[$item['product_id']] ?? 'Unknown',
            'quantity' => (float) $item['quantity'],
            'unit_price' => (float) $item['unit_price'],
            'vat_rate' => (float) ($item['vat_rate'] ?? 0),
            'vat_amount' => (float) $item['unit_price'] * (float) $item['quantity'] * ((float) ($item['vat_rate'] ?? 0) / 100),
            'line_total' => (float) $item['unit_price'] * (float) $item['quantity'],
        ], $data['items']);

        $subtotal = array_sum(array_column($lines, 'line_total'));
        $vatTotal = array_sum(array_column($lines, 'vat_amount'));
        $owedBeforeCredit = $subtotal - $discountInput + $vatTotal;

        $creditNoteIds = array_map('intval', $data['credit_note_ids'] ?? []);

        $credit = $this->resolveCheckoutCredit(
            $customerId,
            $creditNoteIds,
            $owedBeforeCredit,
            app(CustomerCreditService::class),
        );

        $splits = array_map(fn (array $p): array => [
            'method' => $p['method'],
            'amount' => (float) $p['amount'],
            'reference' => $p['reference'] ?? null,
            'cheque_number' => $p['cheque_number'] ?? null,
            'cheque_bank' => $p['cheque_bank'] ?? null,
            'cheque_branch_code' => $p['cheque_branch'] ?? null,
            'cheque_date' => $p['cheque_date'] ?? null,
        ], $data['payments'] ?? []);

        // Credit is a tender, so it joins the other payment splits rather than
        // being folded into the discount. The invoice is still worth its full
        // value; the notes are how part of it got settled.
        $splits = array_merge($splits, $credit['splits']);

        try {
            // Invoice creation and credit-note redemption must commit or fail
            // together — see the source controller's comment: otherwise a losing
            // request in a concurrent double-submit leaves its invoice committed
            // with the discount applied while the matching debit never lands.
            return DB::transaction(fn (): Invoice => app(InvoiceService::class)->create(
                header: [
                    'channel' => 'sales_order',
                    'branch_id' => (int) $data['branch_id'],
                    'customer_id' => $customerId,
                    'discount' => $discountInput,
                    'voucher_codes' => static::parseVoucherCodes($data['voucher_codes'] ?? null),
                    'redeem_points' => (float) ($data['redeem_points'] ?? 0),
                    'note' => $data['note'] ?? null,
                ],
                lines: $lines,
                // The credit splits are in here; PaymentService draws each note
                // down as it books them.
                splits: $splits,
                confirmShortfall: true,
            ));
        } catch (InsufficientStockException $e) {
            throw ValidationException::withMessages(['data.items' => $e->getMessage()]);
        } catch (RuntimeException $e) {
            throw ValidationException::withMessages(['data.credit_note_ids' => $e->getMessage()]);
        }
    }

    /**
     * The source takes voucher_codes as an array; the Filament form collects
     * them as one comma-separated field, so it is split back into the same
     * array shape InvoiceService::create() expects.
     *
     * @return array<int, string>
     */
    public static function parseVoucherCodes(?string $codes): array
    {
        if (! $codes) {
            return [];
        }

        return array_values(array_filter(array_map('trim', explode(',', $codes))));
    }

    protected function getRedirectUrl(): string
    {
        return InvoiceResource::getUrl('view', ['record' => $this->record]);
    }
}
