<?php

namespace App\Filament\Resources\InvoiceResource\Pages;

use App\Filament\Resources\InvoiceResource;
use App\Filament\Support\Perm;
use App\Filament\Support\ProductSelect;
use App\Models\Catalog\Product;
use App\Models\Returns\CreditNote;
use App\Models\Sales\Invoice;
use App\Models\Sales\InvoiceItem;
use App\Services\BatchService;
use App\Services\PaymentService;
use App\Services\VatSettings;
use App\Support\Quantity;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Pages\EditRecord;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Schema;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

/**
 * Ported from InvoiceController::edit()/update(). Only the note, the overall
 * discount, and the line items are editable — matching the source form exactly.
 *
 * Two source behaviours preserved verbatim:
 *  - the submitted unit_price is UI convenience only: a line's price always
 *    comes from the receipt lot (the FEFO-front sellable lot at the invoice's
 *    branch, mirroring InvoiceService::withAuthoritativePrice), never the
 *    client. This also closes the cross-tenant IDOR the `exists:products,id`
 *    rule misses, since the tenant-scoped lookup cannot resolve a foreign
 *    tenant's product.
 *  - the discount can never be reduced below the value already applied via
 *    claimed credit notes.
 *
 * Additionally: without `isms_invoices.discount` the discount field is hidden
 * and any submitted value is discarded in favour of the invoice's own — a
 * hidden field can never silently change it, same rule SalesOrderResource
 * enforces for orders.
 */
class EditInvoice extends EditRecord
{
    protected static string $resource = InvoiceResource::class;

    public function form(Schema $schema): Schema
    {
        return $schema->components([
            Section::make()->schema([
                TextInput::make('discount')
                    ->numeric()->minValue(0)->maxValue(99999999.99)->prefix('Rs.')
                    ->visible(fn (): bool => Perm::can('isms_invoices', 'discount')),
                Textarea::make('note')->maxLength(500)->columnSpanFull(),
            ])->columns(2),
            Section::make('Items')->schema([
                // Deliberately NOT ->relationship(): the source's update()
                // does its own keep-ids diff, catalog reprice and header
                // recompute inside one transaction, so Filament must not also
                // save these rows itself. Rows are loaded in
                // mutateFormDataBeforeFill() and written in handleRecordUpdate().
                Repeater::make('items')
                    ->schema([
                        ProductSelect::standalone(
                            modifyQueryUsing: fn ($query) => $query->where('is_active', true),
                        )->label('Product')->required()->live()->columnSpanFull(),
                        Hidden::make('id'),
                        TextInput::make('quantity')
                            ->numeric()->required()->minValue(0.01)->maxValue(1000000)
                            ->suffix(fn (Get $get): ?string => Quantity::unitLabel(Product::find($get('product_id'))?->baseUnit)),
                        TextInput::make('unit_price')
                            ->numeric()->required()->minValue(0)->maxValue(99999999.99)
                            ->prefix('Rs.')
                            ->helperText('Reset to the lot price on save.'),
                        TextInput::make('vat_rate')
                            ->numeric()->minValue(0)->maxValue(100)->default(0)->suffix('%')
                            ->visible(fn (): bool => app(VatSettings::class)->enabled()),
                    ])
                    ->columns(3)
                    ->minItems(1)
                    ->columnSpanFull(),
            ]),
        ]);
    }

    protected function getHeaderActions(): array
    {
        return [];
    }

    /**
     * Hydrate the (non-relationship) items repeater from the invoice's own
     * rows, carrying each row's `id` so handleRecordUpdate() can tell an
     * existing line from a newly added one — the same distinction the source's
     * `items.*.id` nullable rule expresses.
     *
     * @param  array<string, mixed>  $data
     * @return array<string, mixed>
     */
    protected function mutateFormDataBeforeFill(array $data): array
    {
        /** @var Invoice $invoice */
        $invoice = $this->record;

        $data['items'] = $invoice->items()->get()->map(fn (InvoiceItem $item): array => [
            'id' => $item->id,
            'product_id' => $item->product_id,
            'quantity' => (float) $item->quantity,
            'unit_price' => (float) $item->unit_price,
            'vat_rate' => (float) $item->vat_rate,
        ])->values()->all();

        return $data;
    }

    /**
     * Replaces the whole update path — Filament's default would save the
     * repeater rows itself with the client's prices, which is exactly what the
     * source refuses to do.
     *
     * @param  array<string, mixed>  $data
     */
    protected function handleRecordUpdate(Model $record, array $data): Model
    {
        /** @var Invoice $invoice */
        $invoice = $record;

        // The field is hidden without the permission, but a crafted payload
        // could still submit a changed value — discarded in favour of the
        // record's own, so the discount can only ever be touched by someone
        // who holds isms_invoices.discount.
        if (! Auth::user()?->hasPermissionTo('isms_invoices.discount')) {
            $data['discount'] = (float) $invoice->discount;
        }

        $items = $data['items'] ?? [];

        // Credit notes claimed against this invoice already drew their value
        // from the customer's credit pool and can't be un-claimed here.
        $creditAppliedTotal = (float) CreditNote::where('claimed_invoice_id', $invoice->id)->sum('applied_amount');
        $requestedDiscount = (float) ($data['discount'] ?? $invoice->discount);
        if ($requestedDiscount < $creditAppliedTotal - 0.001) {
            throw ValidationException::withMessages([
                'data.discount' => 'Discount cannot be reduced below '.number_format($creditAppliedTotal, 2).' already applied via credit notes.',
            ]);
        }

        $vatEnabled = app(VatSettings::class)->enabled();

        DB::transaction(function () use ($data, $invoice, $items, $vatEnabled): void {
            $keepIds = collect($items)->pluck('id')->filter()->values()->all();
            $invoice->items()->whereNotIn('id', $keepIds)->delete();

            $products = Product::whereIn('id', collect($items)->pluck('product_id'))
                ->get(['id', 'name'])
                ->keyBy('id');

            foreach ($items as $item) {
                $product = $products->get((int) $item['product_id']);
                if (! $product) {
                    abort(422, "Product #{$item['product_id']} not found.");
                }

                // The form field is hidden while VAT is off, but a crafted
                // payload could still submit a rate — the same "hidden
                // control, enforced server-side" rule PosSettings uses.
                $vatRate = $vatEnabled ? (float) ($item['vat_rate'] ?? 0) : 0.0;
                // The lot, not the product, owns the price: edited lines are
                // re-priced from the FEFO-front sellable lot at the invoice's
                // branch, mirroring the withAuthoritativePrice choke point.
                $lot = app(BatchService::class)->sellableFor((int) $product->id, (int) $invoice->branch_id)->first();
                if (! $lot) {
                    throw ValidationException::withMessages([
                        'items' => "Product #{$product->id} has no sellable lot at this branch.",
                    ]);
                }
                $unitPrice = (float) $lot->sale_price;
                $lineTotal = $unitPrice * (float) $item['quantity'];
                $vatAmount = $lineTotal * ($vatRate / 100);
                $attrs = [
                    'product_id' => $product->id,
                    'product_name' => $product->name,
                    'quantity' => (float) $item['quantity'],
                    'unit_price' => $unitPrice,
                    'vat_rate' => $vatRate,
                    'vat_amount' => $vatAmount,
                    'line_total' => $lineTotal,
                ];

                if (! empty($item['id'])) {
                    InvoiceItem::where('id', $item['id'])
                        ->where('invoice_id', $invoice->id)
                        ->update($attrs);
                } else {
                    InvoiceItem::create(array_merge($attrs, ['invoice_id' => $invoice->id]));
                }
            }

            $invoice->refresh();
            $subtotal = (float) $invoice->items->sum('line_total');
            $vatTotal = (float) $invoice->items->sum('vat_amount');
            $discount = (float) ($data['discount'] ?? $invoice->discount);
            $netTotal = max(0, $subtotal - $discount + $vatTotal);

            // Money already taken cannot be edited away: shrinking an invoice
            // below what the customer has paid would manufacture an overpaid
            // invoice out of thin air. The excess belongs to the customer as
            // credit — process a return or issue a credit note instead.
            $paidAmount = (float) $invoice->paid_amount;
            if ($netTotal + 0.001 < $paidAmount) {
                throw ValidationException::withMessages([
                    'data.items' => 'The revised invoice total (Rs. '.number_format($netTotal, 2)
                        .') is below what the customer has already paid (Rs. '.number_format($paidAmount, 2)
                        .'). Issue a credit note or process a return instead of reducing the invoice below its payments.',
                ]);
            }

            $balance = $netTotal - (float) $invoice->paid_amount;

            $invoice->update([
                'note' => $data['note'] ?? $invoice->note,
                'subtotal' => $subtotal,
                'vat_total' => $vatTotal,
                'total' => $subtotal + $vatTotal,
                'discount' => $discount,
                'net_total' => $netTotal,
                'invoice_payable' => $netTotal,
                'balance' => max(0, $balance),
            ]);
        });

        app(PaymentService::class)->recomputeInvoicePayment($invoice);

        return $invoice->refresh();
    }

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