<?php

namespace App\Filament\Resources;

use App\Filament\Exports\SalesOrderExporter;
use App\Filament\Forms\Components\SalesOrderProductGrid;
use App\Filament\Resources\Concerns\HasModulePermissions;
use App\Filament\Resources\Concerns\HasStandardTableFeatures;
use App\Filament\Resources\SalesOrderResource\Pages\CreateSalesOrder;
use App\Filament\Resources\SalesOrderResource\Pages\EditSalesOrder;
use App\Filament\Resources\SalesOrderResource\Pages\ListSalesOrders;
use App\Filament\Resources\SalesOrderResource\Pages\ViewSalesOrder;
use App\Filament\Support\Chip;
use App\Filament\Support\CustomerSelect;
use App\Filament\Support\Perm;
use App\Filament\Support\RelationSelect;
use App\Filament\Support\StatusLookup;
use App\Models\Lookups\PaymentMethod;
use App\Models\Lookups\Status;
use App\Models\Returns\CreditNote;
use App\Models\Sales\SalesOrder;
use App\Models\User;
use App\Services\AppSettings;
use App\Services\CurrentContext;
use App\Services\SalesOrderService;
use App\Services\VatSettings;
use Filament\Actions\Action;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\Layout\View;
use Filament\Tables\Columns\SelectColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Enums\FiltersLayout;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Livewire\Component as Livewire;
use RuntimeException;

/**
 * Order capture: what a customer has asked for, when it is due, and how far
 * along it is — deliberately *not* a checkout.
 *
 * A sales order is now its own document ({@see SalesOrder}) rather than an
 * Invoice wearing a channel label. That is what lets an order exist before any
 * invoice does: it can sit in production for weeks, take advances, be edited,
 * carry a delivery deadline, and hold made-to-order lines that never touch
 * stock. Money and stock only move at the "Invoice this order" action, which
 * hands the whole thing to InvoiceService untouched.
 *
 * The listing is the working board for the order desk: status is editable in
 * place, payment status is tracked independently alongside it, and a deadline
 * inside the tenant's warning window (Sales Order Settings) reads red.
 */
class SalesOrderResource extends Resource
{
    use HasModulePermissions;
    use HasStandardTableFeatures;

    protected static ?string $model = SalesOrder::class;

    protected static string $permissionModuleKey = 'isms_sales_orders';

    protected static string $exporter = SalesOrderExporter::class;

    protected static string|\BackedEnum|null $navigationIcon = Heroicon::OutlinedClipboardDocumentList;

    protected static string|\UnitEnum|null $navigationGroup = 'Sales';

    protected static ?string $navigationLabel = 'Sales Orders';

    protected static ?int $navigationSort = 0;

    protected static ?string $slug = 'sales-orders';

    protected static ?string $recordTitleAttribute = 'order_number';

    public static function canViewAll(): bool
    {
        return Auth::user()?->hasPermissionTo('isms_sales_orders.view_all') ?? false;
    }

    /**
     * Reps may only open orders they took themselves, unless they hold view_all.
     */
    public static function ownsOrViewAll(Model $record): bool
    {
        return static::canViewAll() || $record->created_by === Auth::id();
    }

    public static function canView(Model $record): bool
    {
        return static::hasModulePermission('view') && static::ownsOrViewAll($record);
    }

    /**
     * An order can be edited right up until it is cancelled. A converted order
     * is still editable — the invoice is resynced from it by
     * SalesOrderService::update() — but its branch, customer and advance
     * payments are fixed, because those were banked at conversion.
     */
    public static function canEdit(Model $record): bool
    {
        return static::hasModulePermission('edit')
            && static::ownsOrViewAll($record)
            && $record->isEditable();
    }

    public static function canDelete(Model $record): bool
    {
        return static::hasModulePermission('delete')
            && static::ownsOrViewAll($record)
            && $record->isOpen();
    }

    public static function form(Schema $schema): Schema
    {
        return $schema->components([
            Section::make('Order')
                ->description('Who it is for, where it is being made, and when it is due.')
                ->schema([
                    CustomerSelect::make('customer_id')
                        ->required()
                        ->helperText('Search by phone number, name or NIC — or add a new customer with the + button.'),
                    RelationSelect::make(
                        name: 'branch_id',
                        relationship: 'branch',
                        titleAttribute: 'name',
                        modifyQueryUsing: fn ($query) => $query->where('is_active', true),
                    )
                        ->label('Branch')
                        ->required()
                        // The product grid is priced per branch and fetches its
                        // own catalog (it is wire:ignore'd, so a re-render can
                        // never hand it one). This is the announcement it
                        // listens for — the only thing ->live() is needed for
                        // here, since nothing else on the form reads the branch.
                        ->live()
                        ->afterStateUpdated(fn ($state, Livewire $livewire) => $livewire->dispatch(
                            'sales-order-branch-changed',
                            branchId: $state === null || $state === '' ? null : (int) $state,
                        )),
                    RelationSelect::make(
                        name: 'sales_rep_id',
                        relationship: 'rep',
                        titleAttribute: 'full_name',
                        modifyQueryUsing: fn ($query) => $query->where('is_active', true),
                    )->label('Sales rep'),
                    DatePicker::make('order_date')
                        ->label('Order date')
                        ->required()
                        ->default(fn (): string => now()->toDateString())
                        ->maxDate(fn (): string => now()->addYear()->toDateString()),
                    DatePicker::make('deadline_date')
                        ->label('Delivery deadline')
                        ->helperText('When the customer needs it. Flagged in the listing as it approaches.')
                        ->minDate(fn (Get $get): ?string => $get('order_date'))
                        ->afterOrEqual('order_date'),
                    Select::make('status_code')
                        ->label('Status')
                        ->options(fn (): array => static::assignableStatusOptions())
                        ->default(Status::SO_CONFIRMED)
                        ->required(),
                    Textarea::make('note')->maxLength(500)->columnSpanFull(),
                ])
                // Full width rather than the page's default 2-up grid: this
                // section and Items below it each need the ENTIRE viewport to
                // lay out their own internal columns properly. Squeezed into
                // half the page (the previous behaviour), the fields below
                // still tried to lay out at their `lg` breakpoint — sized for
                // full-viewport width, not the half they actually had — which
                // is what made both sections look cramped. Only Order
                // discount / Advance payments stay side by side below; they
                // are small enough that half the page is genuinely enough
                // room for them.
                ->columnSpanFull()
                ->columns(['default' => 1, 'sm' => 2, 'lg' => 3]),

            Section::make('Items')
                ->description('Pick from the catalog, or add a made-to-order custom item.')
                ->schema([
                    SalesOrderProductGrid::make('items')->label('Products'),
                ])
                ->columnSpanFull(),

            Section::make('Order discount')
                ->description(fn (): string => Perm::can('isms_sales_orders', 'discount')
                    ? 'A discount on the whole order, on top of any line discounts.'
                    : 'Requires the "Sales Orders — discount" permission.')
                ->schema([
                    Select::make('discount_type')
                        ->label('Discount type')
                        ->options(['fixed' => 'Fixed amount', 'percentage' => 'Percentage'])
                        ->live()
                        ->afterStateUpdated(fn (?string $state, Set $set) => $state ? null : $set('discount_value', 0)),
                    TextInput::make('discount_value')
                        ->label('Discount')
                        ->numeric()->minValue(0)->maxValue(99999999.99)->default(0)
                        ->prefix(fn (Get $get): string => $get('discount_type') === 'percentage' ? '%' : 'Rs.')
                        ->disabled(fn (Get $get): bool => ! $get('discount_type'))
                        ->dehydrated(),
                ])
                ->columns(2)
                ->collapsed()
                // A discount on an order is a privilege: without the
                // `isms_sales_orders.discount` permission the section does not
                // render at all. The save path is guarded server-side too
                // (SalesOrderService::assertDiscountEditsAllowed()), and the
                // service preserves a discount already on the record when the
                // editor lacks the permission — hiding the section must never
                // silently strip an existing discount.
                ->visible(fn (): bool => Perm::can('isms_sales_orders', 'discount')),

            Section::make('Advance payments')
                ->description('Optional. An order can be taken with nothing paid — leave this empty and invoice it later.')
                ->schema([
                    Repeater::make('payments')
                        ->label('')
                        ->schema([
                            Select::make('method')
                                ->options(fn (): array => static::paymentMethodOptions())
                                ->required()
                                ->live()
                                ->afterStateUpdated(fn ($state, Set $set) => static::clearPaymentFields($set)),
                            Select::make('credit_note_id')
                                ->label('Credit note')
                                ->options(fn (Get $get): array => static::issuedCreditNoteOptions($get('../../customer_id')))
                                ->visible(fn (Get $get): bool => $get('method') === PaymentMethod::CREDIT)
                                ->required(fn (Get $get): bool => $get('method') === PaymentMethod::CREDIT)
                                ->live()
                                // Pre-fills the note's remaining balance as a
                                // convenient default — the rep is free to type
                                // less. A note is spent down over time, so part
                                // of it can settle this order and the rest stay
                                // on the note for the next one.
                                ->afterStateUpdated(fn ($state, Set $set) => $set(
                                    'amount',
                                    $state ? CreditNote::find($state)?->remainingAmount() : null,
                                ))
                                ->helperText('Drawn down when the order is invoiced. Enter any amount up to what is left on the note.'),
                            TextInput::make('amount')
                                ->numeric()->required()->minValue(0.01)->prefix('Rs.')
                                // Advances are capped at the order's value (the
                                // service enforces the same rule authoritatively
                                // on save) — a Rs. 2000 "advance" on a Rs. 400
                                // order is a mistake, not a deposit. A credit
                                // advance is additionally capped at what the
                                // chosen note still holds.
                                ->maxValue(fn (Get $get): float => max(0.01, static::creditAwareMaxAdvance($get)))
                                ->helperText(fn (Get $get): string => static::advanceHelperText($get)),
                            DatePicker::make('received_at')
                                ->label('Received on')
                                ->default(fn (): string => now()->toDateString()),
                            // The reference / cheque / custom fields below are
                            // shown only when the chosen payment method actually
                            // needs them (PaymentMethod's own
                            // requires_reference / requires_cheque_fields flags
                            // and custom-method `fields` config) — the same
                            // rules the POS checkout enforces.
                            TextInput::make('reference')
                                ->maxLength(100)
                                ->visible(fn (Get $get): bool => static::shouldShowReferenceField($get))
                                ->required(fn (Get $get): bool => static::selectedMethodRequiresReference($get)),
                            TextInput::make('cheque_number')
                                ->maxLength(50)
                                ->visible(fn (Get $get): bool => static::selectedMethodRequiresChequeFields($get))
                                ->required(fn (Get $get): bool => static::selectedMethodRequiresChequeFields($get)),
                            TextInput::make('cheque_bank')
                                ->maxLength(100)
                                ->visible(fn (Get $get): bool => static::selectedMethodRequiresChequeFields($get))
                                ->required(fn (Get $get): bool => static::selectedMethodRequiresChequeFields($get)),
                            TextInput::make('cheque_branch')
                                ->maxLength(100)
                                ->visible(fn (Get $get): bool => static::selectedMethodRequiresChequeFields($get)),
                            DatePicker::make('cheque_date')
                                ->visible(fn (Get $get): bool => static::selectedMethodRequiresChequeFields($get))
                                ->required(fn (Get $get): bool => static::selectedMethodRequiresChequeFields($get)),
                            ...static::paymentCustomFieldComponents(),
                        ])
                        // Same reasoning as SalesOrderProductGrid's own fix above:
                        // this Section is one of two side by side on the page, so
                        // 3 columns of fields (some with longer labels like "Cheque
                        // bank") only had roughly half the viewport to work with.
                        ->columns(2)
                        ->defaultItems(1)
                        ->addActionLabel('Record an advance')
                        ->columnSpanFull(),
                ])->collapsed(),
        ]);
    }

    /**
     * Everything except `invoiced`, which is reached only by converting the
     * order — never by picking the word out of a dropdown.
     *
     * @return array<string, string>
     */
    public static function assignableStatusOptions(): array
    {
        return array_diff_key(
            StatusLookup::options(Status::CTX_SALES_ORDER),
            [Status::SO_INVOICED => null],
        );
    }

    /**
     * @return array<string, string>
     */
    public static function paymentMethodOptions(): array
    {
        return PaymentMethod::checkoutOptions() + [PaymentMethod::CREDIT => 'Credit note'];
    }

    /**
     * The field config every payment entry point on an order reads from —
     * exactly the `requires_reference` / `requires_cheque_fields` flags and
     * custom-method `fields` config the POS checkout renders, so the order
     * desk asks for the same details the terminal would.
     *
     * @return array<string, array{requires_reference: bool, requires_cheque_fields: bool, fields: array<int, array<string, mixed>>}>
     */
    public static function paymentMethodConfig(): array
    {
        return PaymentMethod::active()
            ->mapWithKeys(fn (PaymentMethod $method): array => [$method->code => [
                'requires_reference' => (bool) $method->requires_reference,
                'requires_cheque_fields' => (bool) $method->requires_cheque_fields,
                'fields' => $method->fields ?? [],
            ]])
            ->all();
    }

    private static function selectedMethodConfig(Get $get): ?array
    {
        $code = (string) $get('method');

        return $code === '' ? null : (static::paymentMethodConfig()[$code] ?? null);
    }

    private static function selectedMethodRequiresChequeFields(Get $get): bool
    {
        return (bool) (static::selectedMethodConfig($get)['requires_cheque_fields'] ?? false);
    }

    private static function selectedMethodRequiresReference(Get $get): bool
    {
        return (bool) (static::selectedMethodConfig($get)['requires_reference'] ?? false);
    }

    /**
     * Card and bank transfer ask for a reference at the POS regardless of the
     * flag (the flag only decides whether it is mandatory) — mirror that here
     * so the two entry points collect the same detail.
     */
    private static function shouldShowReferenceField(Get $get): bool
    {
        return in_array((string) $get('method'), [PaymentMethod::CARD, PaymentMethod::BANK_TRANSFER], true)
            || static::selectedMethodRequiresReference($get);
    }

    /**
     * @return array<int, string>
     */
    private static function selectedMethodFieldKeys(Get $get): array
    {
        $config = static::selectedMethodConfig($get);

        return $config ? array_column($config['fields'], 'key') : [];
    }

    /**
     * One component per distinct custom-field key declared across every active
     * payment method, shown only while the chosen method actually declares it.
     * File-type fields are captured by the POS terminal (uploaded into tenant
     * storage), not the order desk, so they are skipped here — and the service
     * ignores them when validating an advance.
     *
     * Works in both the form's payments repeater (paths resolve against the
     * repeater item) and the add-payment modal (paths resolve at the top
     * level), since the state path is always `custom_fields.<key>`.
     *
     * @return array<int, Component>
     */
    public static function paymentCustomFieldComponents(): array
    {
        $byKey = [];

        foreach (PaymentMethod::active() as $method) {
            foreach ($method->fields ?? [] as $field) {
                if (($field['type'] ?? 'text') === 'file') {
                    continue;
                }

                $key = (string) ($field['key'] ?? '');

                if ($key === '') {
                    continue;
                }

                $current = $byKey[$key] ?? null;

                if ($current === null || (empty($current['required']) && ! empty($field['required']))) {
                    $byKey[$key] = [
                        'label' => (string) ($field['label'] ?? ucwords(str_replace('_', ' ', $key))),
                        'type' => (string) ($field['type'] ?? 'text'),
                        'required' => ! empty($field['required']),
                        'options' => array_values($field['options'] ?? []),
                    ];
                }
            }
        }

        $components = [];

        foreach ($byKey as $key => $field) {
            $belongsToMethod = fn (Get $get): bool => in_array($key, static::selectedMethodFieldKeys($get), true);

            $component = match ($field['type']) {
                'dropdown' => Select::make("custom_fields.{$key}")
                    ->options(array_combine($field['options'], $field['options'])),
                'number' => TextInput::make("custom_fields.{$key}")->numeric(),
                default => TextInput::make("custom_fields.{$key}"),
            };

            $components[] = $component
                ->label($field['label'])
                ->visible($belongsToMethod)
                ->required(fn (Get $get): bool => $field['required'] && $belongsToMethod($get))
                ->live();
        }

        return $components;
    }

    /**
     * Switching the payment method must not leave stale tender details behind —
     * a cheque number picked for "cheque" has no business riding along on a
     * cash advance, and the persisted payment would otherwise carry it.
     */
    public static function clearPaymentFields(Set $set): void
    {
        $set('reference', null);
        $set('cheque_number', null);
        $set('cheque_bank', null);
        $set('cheque_branch', null);
        $set('cheque_date', null);
        $set('credit_note_id', null);
        $set('custom_fields', []);
    }

    /**
     * Issued notes belonging to this order's customer — a note can only ever be
     * claimed against its own customer's invoice, so offering anyone else's
     * would only produce a failure at conversion.
     *
     * @return array<int, string>
     */
    public static function issuedCreditNoteOptions(mixed $customerId): array
    {
        if (! $customerId) {
            return [];
        }

        return CreditNote::query()
            ->where('customer_id', $customerId)
            // Spendable, not merely issued: a part-used note stays issued and
            // must be offered at what is LEFT on it, not its face value.
            ->spendable()
            ->orderBy('id')
            ->get()
            ->mapWithKeys(fn (CreditNote $cn): array => [$cn->id => $cn->pickerLabel()])
            ->all();
    }

    /**
     * What the chosen note still holds, or null when this row is not a credit
     * payment (or no note is picked yet).
     */
    public static function remainingOnSelectedNote(Get $get): ?float
    {
        if ($get('method') !== PaymentMethod::CREDIT) {
            return null;
        }

        $noteId = $get('credit_note_id');

        return $noteId ? CreditNote::find($noteId)?->remainingAmount() : null;
    }

    /**
     * A credit advance is bounded by two things at once: the order's own value
     * and what is left on the note being spent. The lower of the two wins.
     */
    public static function creditAwareMaxAdvance(Get $get): float
    {
        $orderCap = static::estimateNetTotal($get);
        $noteCap = static::remainingOnSelectedNote($get);

        return $noteCap === null ? $orderCap : min($orderCap, $noteCap);
    }

    public static function advanceHelperText(Get $get): string
    {
        $text = 'Order total: Rs. '.number_format(static::estimateNetTotal($get), 2).' — advances cannot exceed it.';

        $noteCap = static::remainingOnSelectedNote($get);

        if ($noteCap !== null) {
            $text .= ' This credit note has Rs. '.number_format($noteCap, 2).' left.';
        }

        return $text;
    }

    /**
     * The order's net total as the form can estimate it from the grid state —
     * the same maths as SalesOrderService::recalculate(), so the advance cap
     * shown here matches what the service enforces on save.
     */
    public static function estimateNetTotal(Get $get): float
    {
        $vatEnabled = app(VatSettings::class)->enabled();
        $subtotal = 0.0;
        $lineDiscounts = 0.0;
        $vatTotal = 0.0;

        foreach ($get('../../items') ?? [] as $item) {
            $gross = (float) ($item['unit_price'] ?? 0) * (float) ($item['quantity'] ?? 0);
            $subtotal += $gross;
            $lineDiscounts += static::lineDiscountAmount(
                $gross,
                $item['discount_type'] ?? null,
                (float) ($item['discount_value'] ?? 0),
            );
            if ($vatEnabled) {
                $vatTotal += $gross * ((float) ($item['vat_rate'] ?? 0) / 100);
            }
        }

        $netOfLines = max(0, $subtotal - $lineDiscounts);
        $headerDiscount = static::lineDiscountAmount(
            $netOfLines,
            $get('../../discount_type'),
            (float) ($get('../../discount_value') ?? 0),
        );

        return round(max(0, $netOfLines - $headerDiscount + $vatTotal), 2);
    }

    /**
     * Clamp a fixed/percentage discount to the amount it comes off — mirrors
     * the rule SalesOrderService applies when the order is actually saved.
     */
    private static function lineDiscountAmount(float $amount, mixed $type, float $value): float
    {
        if (! $type || $value <= 0 || $amount <= 0) {
            return 0.0;
        }

        $discount = $type === 'percentage' ? $amount * ($value / 100) : $value;

        return round(min(max($discount, 0), $amount), 2);
    }

    public static function table(Table $table): Table
    {
        $warningDays = app(AppSettings::class)->salesOrderDeadlineWarningDays();

        return static::standardTableDefaults($table)
            ->recordUrl(static::standardRecordUrl())
            ->emptyStateHeading('No sales orders yet')
            ->emptyStateDescription('Orders taken at the till or the order desk will appear here.')
            ->modifyQueryUsing(fn (Builder $query): Builder => $query
                ->with(['customer', 'branch', 'status', 'paymentStatus', 'creator'])
                ->when(! static::canViewAll(), fn (Builder $q) => $q->where('created_by', Auth::id())))
            ->columns([
                View::make('filament.tables.sales-order-card')
                    ->components([
                        Chip::documentNumber('order_number', 'Order #')
                            ->searchable()
                            ->sortable(),

                        TextColumn::make('customer.business_name')
                            ->label('Customer')
                            ->searchable()
                            ->placeholder('—')
                            ->description(fn (SalesOrder $record): ?string => $record->customer?->phone),

                        TextColumn::make('branch.name')
                            ->label('Branch')
                            ->placeholder('—')
                            ->toggleable(),

                        TextColumn::make('order_date')
                            ->label('Ordered')
                            ->date()
                            ->sortable(),

                        // The deadline is the column this whole listing exists for, so
                        // it carries its own urgency rather than making someone read
                        // dates and do the arithmetic.
                        TextColumn::make('deadline_date')
                            ->label('Deadline')
                            ->date()
                            ->sortable()
                            ->placeholder('—')
                            ->badge()
                            ->color(fn (SalesOrder $record): string => static::deadlineColor($record, $warningDays))
                            ->icon(fn (SalesOrder $record): ?Heroicon => static::deadlineIsUrgent($record, $warningDays)
                                ? Heroicon::OutlinedExclamationTriangle
                                : null)
                            ->description(fn (SalesOrder $record): ?string => static::deadlineNote($record)),

                        // Status is editable straight from the listing — the order desk
                        // works this board all day and should not have to open a record
                        // to say "this one's in production now". The write still goes
                        // through SalesOrderService, so the listing cannot reach a state
                        // the detail page would refuse.
                        SelectColumn::make('status_id')
                            ->label('Status')
                            ->options(fn (): array => static::statusIdOptions())
                            ->disabled(fn (SalesOrder $record): bool => ! $record->isEditable()
                                || $record->isConverted()
                                || ! Perm::can('isms_sales_orders', 'update_status')
                                || ! static::ownsOrViewAll($record))
                            ->selectablePlaceholder(false)
                            ->updateStateUsing(
                                fn (SalesOrder $record, $state) => static::applyStatusChange(
                                    $record,
                                    (int) $state
                                )
                            ),

                        TextColumn::make('paymentStatus.name')
                            ->label('Payment')
                            ->badge()
                            ->color(fn (SalesOrder $record): string => StatusLookup::colorOf(
                                Status::CTX_INVOICE_PAYMENT,
                                $record->paymentStatus?->code ?? ''
                            )),

                        TextColumn::make('net_total')
                            ->label('Order value')
                            ->money(fn () => app(CurrentContext::class)->currencyCode())
                            ->sortable(),

                        TextColumn::make('advance_paid')
                            ->label('Paid')
                            ->money(fn () => app(CurrentContext::class)->currencyCode())
                            ->sortable(),

                        TextColumn::make('balance')
                            ->label('Balance')
                            ->money(fn () => app(CurrentContext::class)->currencyCode())
                            ->sortable(),

                        TextColumn::make('items_count')
                            ->label('Lines')
                            ->counts('items'),

                        TextColumn::make('custom_items_count')
                            ->label('Custom')
                            ->counts('customItems')
                            ->badge()
                            ->color('warning')
                            ->placeholder('—'),

                        TextColumn::make('invoice.invoice_number')
                            ->label('Invoice')
                            ->badge()
                            ->color('success')
                            ->placeholder('—'),

                        TextColumn::make('creator.name')
                            ->label('Rep')
                            ->placeholder('—'),

                        TextColumn::make('created_at')
                            ->dateTime()
                            ->sortable(),
                    ])
                    ->columnSpanFull(),
            ])
            ->contentGrid([
                'default' => 1,
            ])
            ->defaultSort('created_at', 'desc')
            ->filters([
                Filter::make('order_number')
                    ->schema([TextInput::make('value')->label('Order #')])
                    ->query(fn (Builder $query, array $data): Builder => $query->when(
                        $data['value'] ?? null,
                        fn (Builder $q, $value) => $q->where('order_number', 'like', "%{$value}%"),
                    )),
                Filter::make('customer')
                    ->schema([TextInput::make('value')->label('Customer name or phone')])
                    ->query(fn (Builder $query, array $data): Builder => $query->when(
                        $data['value'] ?? null,
                        fn (Builder $q, $value) => $q->whereHas('customer', fn ($qq) => $qq
                            ->where('business_name', 'like', "%{$value}%")
                            ->orWhere('phone', 'like', "%{$value}%")),
                    )),
                SelectFilter::make('status')
                    ->label('Status')
                    ->options(fn (): array => StatusLookup::options(Status::CTX_SALES_ORDER))
                    ->query(fn (Builder $query, array $data): Builder => $query->when(
                        $data['value'] ?? null,
                        fn (Builder $q, $code) => $q->whereHas('status', fn ($qq) => $qq->where('code', $code)),
                    )),
                SelectFilter::make('payment_status')
                    ->label('Payment status')
                    ->options(fn (): array => StatusLookup::options(Status::CTX_INVOICE_PAYMENT))
                    ->query(fn (Builder $query, array $data): Builder => $query->when(
                        $data['value'] ?? null,
                        fn (Builder $q, $code) => $q->whereHas('paymentStatus', fn ($qq) => $qq->where('code', $code)),
                    )),
                Filter::make('due_soon')
                    ->label('Due soon or overdue')
                    ->toggle()
                    ->query(fn (Builder $query): Builder => $query
                        ->open()
                        ->whereNotNull('deadline_date')
                        ->whereDate('deadline_date', '<=', now()->addDays($warningDays)->toDateString())),
                Filter::make('has_custom_items')
                    ->label('Has custom items')
                    ->toggle()
                    ->query(fn (Builder $query): Builder => $query->whereHas('items', fn ($q) => $q->where('is_custom', true))),
                SelectFilter::make('rep')
                    ->label('Representative')
                    ->visible(fn (): bool => static::canViewAll())
                    ->options(fn (): array => User::query()
                        ->whereIn('id', SalesOrder::query()->whereNotNull('created_by')->distinct()->pluck('created_by'))
                        ->orderBy('name')
                        ->pluck('name', 'id')
                        ->all())
                    ->query(fn (Builder $query, array $data): Builder => $query->when(
                        $data['value'] ?? null,
                        fn (Builder $q, $rep) => $q->where('created_by', $rep),
                    )),
                Filter::make('deadline_date')
                    ->schema([
                        DatePicker::make('deadline_from')->label('Deadline from'),
                        DatePicker::make('deadline_to')->label('Deadline to'),
                    ])
                    ->query(fn (Builder $query, array $data): Builder => $query
                        ->when($data['deadline_from'] ?? null, fn (Builder $q, $d) => $q->whereDate('deadline_date', '>=', $d))
                        ->when($data['deadline_to'] ?? null, fn (Builder $q, $d) => $q->whereDate('deadline_date', '<=', $d))),
                Filter::make('order_date')
                    ->schema([
                        DatePicker::make('date_from'),
                        DatePicker::make('date_to'),
                    ])
                    ->query(fn (Builder $query, array $data): Builder => $query
                        ->when($data['date_from'] ?? null, fn (Builder $q, $d) => $q->whereDate('order_date', '>=', $d))
                        ->when($data['date_to'] ?? null, fn (Builder $q, $d) => $q->whereDate('order_date', '<=', $d))),
            ])
            ->filtersLayout(FiltersLayout::AboveContentCollapsible)
            ->filtersFormColumns(2)
            ->headerActions(static::standardExportActions())
            ->toolbarActions([])
            ->recordActions([
                Action::make('view')
                    ->icon(Heroicon::OutlinedEye)
                    ->size('lg')
                    ->color('gray')
                    ->button()
                    ->url(fn (SalesOrder $record): string => \App\Filament\Resources\InvoiceResource\Pages\ViewInvoice::getUrl([
                        'record' => $record->invoice,
                    ]))
                    ->visible(fn (SalesOrder $record): bool => static::canView($record) && $record->invoice !== null),
                Action::make('edit')
                    ->icon(Heroicon::OutlinedPencilSquare)
                    ->size('lg')
                    ->color('primary')
                    ->button()
                    ->url(fn (SalesOrder $record): string => EditSalesOrder::getUrl(['record' => $record]))
                    ->visible(fn (SalesOrder $record): bool => static::canEdit($record)),
                static::addPaymentAction(),
                static::convertAction(),
                static::cancelAction(),
            ]);
    }

    /**
     * The one action that makes anything irreversible happen — stock leaves,
     * the invoice number is issued, and advances are banked.
     */
    public static function convertAction(): Action
    {
        return Action::make('convertToInvoice')
            ->label('Invoice')
            ->icon(Heroicon::OutlinedDocumentText)
            ->size('lg')
            ->color('success')
            ->button()
            ->requiresConfirmation()
            ->modalHeading('Invoice this order')
            ->modalDescription('Stock is deducted and any advances are banked. The order is locked once this is done.')
            ->modalSubmitActionLabel('Create invoice')
            ->visible(fn (SalesOrder $record): bool => $record->isOpen()
                && Perm::can('isms_sales_orders', 'invoice')
                && static::ownsOrViewAll($record))
            ->action(function (SalesOrder $record): void {
                try {
                    $invoice = app(SalesOrderService::class)->convertToInvoice($record);
                } catch (RuntimeException $e) {
                    Notification::make()->title('Could not invoice this order')->body($e->getMessage())->danger()->send();

                    return;
                }

                Notification::make()
                    ->title("Invoice {$invoice->invoice_number} created")
                    ->body("Order {$record->order_number} is now invoiced.")
                    ->success()
                    ->send();
            });
    }

    public static function cancelAction(): Action
    {
        return Action::make('cancel')
            ->label('Cancel')
            ->icon(Heroicon::OutlinedXCircle)
            ->color('danger')
            ->size('lg')
            ->button()
            ->requiresConfirmation()
            ->modalHeading('Cancel this order')
            ->schema([
                Textarea::make('reason')->label('Reason')->maxLength(300),
            ])
            ->visible(fn (SalesOrder $record): bool => $record->isOpen()
                && Perm::can('isms_sales_orders', 'cancel')
                && static::ownsOrViewAll($record))
            ->action(function (SalesOrder $record, array $data): void {
                try {
                    app(SalesOrderService::class)->cancel($record, $data['reason'] ?? null);
                } catch (RuntimeException $e) {
                    Notification::make()->title('Could not cancel this order')->body($e->getMessage())->danger()->send();

                    return;
                }

                Notification::make()->title("Order {$record->order_number} cancelled")->success()->send();
            });
    }

    /**
     * Record an advance against an order without opening the whole edit
     * form — the modal asks for exactly the fields the chosen payment method
     * needs (cheque details, reference, custom-method fields), and the write
     * goes through SalesOrderService::recordPayment() with the same rules an
     * edit-form submission would face.
     *
     * Offered on a converted order too: settling the balance after the invoice
     * is issued is the ordinary case. Only a cancelled order is refused, which
     * is exactly the gate recordPayment() enforces server-side — the button
     * must not offer what the service would throw on.
     */
    public static function addPaymentAction(): Action
    {
        return Action::make('addPayment')
            ->label('Add payment')
            ->icon(Heroicon::OutlinedBanknotes)
            ->size('lg')
            ->color('primary')
            ->button()
            ->visible(fn (SalesOrder $record): bool => ! $record->isCancelled()
                && (float) $record->balance > 0.001
                && Perm::can('isms_sales_orders', 'edit')
                && static::ownsOrViewAll($record))
            ->schema([
                Select::make('method')
                    ->options(fn (): array => static::paymentMethodOptions())
                    ->required()
                    ->live()
                    ->afterStateUpdated(fn ($state, Set $set) => static::clearPaymentFields($set)),
                Select::make('credit_note_id')
                    ->label('Credit note')
                    ->options(fn (SalesOrder $record): array => static::issuedCreditNoteOptions($record->customer_id))
                    ->visible(fn (Get $get): bool => $get('method') === PaymentMethod::CREDIT)
                    ->required(fn (Get $get): bool => $get('method') === PaymentMethod::CREDIT)
                    ->live()
                    // Pre-fills what is left on the note as a default; the
                    // desk can take less and leave the rest on the note.
                    ->afterStateUpdated(fn ($state, Set $set) => $set(
                        'amount',
                        $state ? CreditNote::find($state)?->remainingAmount() : null,
                    ))
                    ->helperText('Drawn down when the order is invoiced. Enter any amount up to what is left on the note.'),
                TextInput::make('amount')
                    ->numeric()->required()->minValue(0.01)->prefix('Rs.')
                    ->maxValue(function (SalesOrder $record, Get $get): float {
                        $balanceCap = max(0.01, (float) $record->balance);
                        $noteCap = static::remainingOnSelectedNote($get);

                        return $noteCap === null ? $balanceCap : min($balanceCap, max(0.01, $noteCap));
                    })
                    ->helperText(function (SalesOrder $record, Get $get): string {
                        $text = 'Outstanding balance: Rs. '.number_format((float) $record->balance, 2);
                        $noteCap = static::remainingOnSelectedNote($get);

                        return $noteCap === null
                            ? $text
                            : $text.'. This credit note has Rs. '.number_format($noteCap, 2).' left.';
                    }),
                DatePicker::make('received_at')
                    ->label('Received on')
                    ->default(fn (): string => now()->toDateString()),
                TextInput::make('reference')
                    ->maxLength(100)
                    ->visible(fn (Get $get): bool => static::shouldShowReferenceField($get))
                    ->required(fn (Get $get): bool => static::selectedMethodRequiresReference($get)),
                TextInput::make('cheque_number')
                    ->maxLength(50)
                    ->visible(fn (Get $get): bool => static::selectedMethodRequiresChequeFields($get))
                    ->required(fn (Get $get): bool => static::selectedMethodRequiresChequeFields($get)),
                TextInput::make('cheque_bank')
                    ->maxLength(100)
                    ->visible(fn (Get $get): bool => static::selectedMethodRequiresChequeFields($get))
                    ->required(fn (Get $get): bool => static::selectedMethodRequiresChequeFields($get)),
                TextInput::make('cheque_branch')
                    ->maxLength(100)
                    ->visible(fn (Get $get): bool => static::selectedMethodRequiresChequeFields($get)),
                DatePicker::make('cheque_date')
                    ->visible(fn (Get $get): bool => static::selectedMethodRequiresChequeFields($get))
                    ->required(fn (Get $get): bool => static::selectedMethodRequiresChequeFields($get)),
                ...static::paymentCustomFieldComponents(),
            ])
            ->action(function (SalesOrder $record, array $data): void {
                try {
                    app(SalesOrderService::class)->recordPayment($record, $data);
                } catch (RuntimeException $e) {
                    Notification::make()->title('Could not record the payment')->body($e->getMessage())->danger()->send();

                    return;
                }

                Notification::make()
                    ->title('Payment recorded on order '.$record->order_number.'.')
                    ->success()
                    ->send();
            });
    }

    /**
     * Status options keyed by id, for the in-place SelectColumn. `invoiced` is
     * included so an already-converted order still renders its own status, but
     * the column is disabled on those rows so it can never be picked by hand.
     *
     * @return array<int, string>
     */
    public static function statusIdOptions(): array
    {
        return StatusLookup::forContext(Status::CTX_SALES_ORDER)
            ->mapWithKeys(fn (Status $status): array => [$status->id => $status->name])
            ->all();
    }

    /**
     * Apply an in-place status edit from the listing.
     *
     * Re-checks the permission and ownership server-side rather than trusting
     * the column's own ->disabled() (which is only a rendering hint), then hands
     * the change to the service so it is audited exactly like any other route
     * to the same state.
     */
    protected static function applyStatusChange(SalesOrder $record, int $statusId): mixed
    {
        abort_unless(Perm::can('isms_sales_orders', 'update_status'), 403);
        abort_unless(static::ownsOrViewAll($record), 403);

        $code = StatusLookup::forContext(Status::CTX_SALES_ORDER)
            ->first(fn (Status $status): bool => $status->id === $statusId)?->code;

        if ($code === null) {
            Notification::make()->title('Unknown status')->danger()->send();

            return $record->status_id;
        }

        try {
            app(SalesOrderService::class)->updateStatus($record, $code);
        } catch (RuntimeException $e) {
            Notification::make()->title('Status not changed')->body($e->getMessage())->danger()->send();

            return $record->status_id;
        }

        Notification::make()
            ->title("Order {$record->order_number} moved to ".$record->refresh()->status?->name)
            ->success()
            ->send();

        return $statusId;
    }

    /**
     * Red once the deadline is inside the tenant's warning window (Sales Order
     * Settings) and for anything already overdue; amber just outside it.
     * Deadlines on closed orders carry no urgency at all.
     */
    public static function deadlineColor(SalesOrder $record, int $warningDays): string
    {
        $days = $record->daysToDeadline();

        if ($days === null) {
            return 'gray';
        }

        if ($days < 0 || ($warningDays > 0 && $days <= $warningDays)) {
            return 'danger';
        }

        return $warningDays > 0 && $days <= $warningDays * 2 ? 'warning' : 'gray';
    }

    public static function deadlineIsUrgent(SalesOrder $record, int $warningDays): bool
    {
        $days = $record->daysToDeadline();

        return $days !== null && ($days < 0 || ($warningDays > 0 && $days <= $warningDays));
    }

    public static function deadlineNote(SalesOrder $record): ?string
    {
        $days = $record->daysToDeadline();

        return match (true) {
            $days === null => null,
            $days < 0 => abs($days).' day(s) overdue',
            $days === 0 => 'Due today',
            $days === 1 => 'Due tomorrow',
            default => "In {$days} days",
        };
    }

    public static function getPages(): array
    {
        return [
            'index' => ListSalesOrders::route('/'),
            'create' => CreateSalesOrder::route('/create'),
            'view' => ViewSalesOrder::route('/{record}'),
            'edit' => EditSalesOrder::route('/{record}/edit'),
        ];
    }
}