<?php

namespace App\Filament\Resources;

use App\Filament\Resources\Concerns\HasModulePermissions;
use App\Filament\Resources\Concerns\HasStandardTableFeatures;
use App\Filament\Resources\TransactionResource\Pages\CreateTransaction;
use App\Filament\Resources\TransactionResource\Pages\EditTransaction;
use App\Filament\Resources\TransactionResource\Pages\ListTransactions;
use App\Filament\Resources\TransactionResource\Pages\ViewTransaction;
use App\Filament\Support\Chip;
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\Sales\Invoice;
use App\Models\Sales\Transaction;
use App\Models\User;
use App\Services\CurrentContext;
use App\Services\PaymentService;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
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\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\TextColumn;
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 RuntimeException;

/**
 * Ported from App\Http\Controllers\Isms\TransactionController + PaymentService
 * (docs/migration-plan.md Phase 5/6).
 *
 * The cheque lifecycle (pending → approved → utilized | rejected → pending) is
 * the module's core: approve confirms a cleared instrument and adds it to the
 * linked invoices' paid amounts; reject marks a bounced cheque; revert puts a
 * confirmed/rejected transaction back to pending. All three go exclusively
 * through PaymentService, which owns the status transitions, invoice
 * recomputation, and overpayment-credit-note guards.
 *
 * Permission verbs mirror the source routes:
 *  - index            → isms_transactions.access
 *  - show             → isms_transactions.view (+ ownership, or view_all)
 *  - create           → isms_transactions.create
 *  - edit/update      → isms_transactions.approve (source routes carry approve)
 *  - approve / revert → isms_transactions.approve
 *  - reject           → isms_transactions.reject
 *  - destroy          → isms_transactions.delete
 *  - export           → isms_transactions.export
 */
class TransactionResource extends Resource
{
    use HasModulePermissions;
    use HasStandardTableFeatures;

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

    protected static string $permissionModuleKey = 'isms_transactions';

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

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

    protected static ?int $navigationSort = 30;

    protected static ?string $recordTitleAttribute = 'transaction_number';

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

    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);
    }

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

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

    public static function form(Schema $schema): Schema
    {
        return $schema->components([
            Section::make('Payment')->schema([
                RelationSelect::make(
                    name: 'customer_id',
                    relationship: 'customer',
                    titleAttribute: 'business_name',
                    modifyQueryUsing: fn ($query) => $query->where('is_active', true),
                    createPermission: 'isms_customers.create',
                )->label('Customer')
                    ->required()
                    // Picking a customer surfaces their outstanding balance and
                    // pre-fills the payment amount with it when nothing has been
                    // typed yet — a plain "settle the account" payment should
                    // never require arithmetic or re-typing a figure.
                    ->live()
                    ->afterStateUpdated(function (Get $get, Set $set, $state): void {
                        if (! $state) {
                            return;
                        }

                        if (blank($get('amount')) || (float) $get('amount') <= 0) {
                            $outstanding = static::customerOutstanding((int) $state);
                            if ($outstanding > 0) {
                                $set('amount', $outstanding);
                            }
                        }
                    }),
                Select::make('branch_id')
                    ->label('Branch')
                    ->relationship('branch', 'name')
                    ->searchable()
                    ->preload(),
                Select::make('payment_method_id')
                    ->label('Payment method')
                    ->relationship('paymentMethod', 'name', modifyQueryUsing: fn ($query) => $query->where('is_active', true))
                    ->searchable()
                    ->preload()
                    ->required(),
                TextInput::make('amount')
                    ->numeric()->required()->minValue(0.01)->maxValue(99999999.99)->prefix('Rs.')
                    // Live summary of what the customer still owes, what this
                    // payment leaves behind, and how much of it is allocated —
                    // so a typo'd figure is visible before it is saved.
                    ->live(debounce: 500)
                    ->hint(fn (Get $get): ?string => static::amountHint($get))
                    ->hintAction(
                        Action::make('fill_outstanding')
                            ->label('Use outstanding')
                            ->icon(Heroicon::OutlinedSparkles)
                            ->color('primary')
                            ->visible(fn (Get $get): bool => (bool) $get('customer_id'))
                            ->action(fn (Get $get, Set $set): mixed => $set('amount', static::customerOutstanding((int) $get('customer_id')))),
                    ),
                DatePicker::make('taken_on')
                    ->label('Taken on')
                    ->helperText('Leave blank to use the current time.'),
                TextInput::make('reference')->maxLength(100),
            ])->columns(3),

            Section::make('Cheque details')
                ->visible(fn (callable $get): bool => static::isChequeMethod($get('payment_method_id')))
                ->schema([
                    TextInput::make('cheque_number')->maxLength(50),
                    TextInput::make('cheque_bank')->maxLength(100),
                    DatePicker::make('cheque_date'),
                ])->columns(3),

            Section::make('Invoice allocation')
                ->description('Settle unpaid invoices oldest-first. Unallocated amounts become a customer credit note on non-cheque transactions.')
                ->schema([
                    Repeater::make('invoice_allocations')
                        ->label('Allocate to invoices')
                        ->schema([
                            Select::make('invoice_id')
                                ->label('Invoice')
                                ->options(function (Get $get): array {
                                    $customerId = (int) ($get('../../customer_id') ?? $get('../customer_id') ?? 0);
                                    $options = static::unpaidInvoiceOptions($customerId);

                                    // Exclude invoices already picked in other rows so the
                                    // same invoice cannot be allocated twice.
                                    $allocations = $get('../../invoice_allocations') ?? $get('../invoice_allocations') ?? $get('invoice_allocations') ?? [];
                                    // Fallback: try to read the full form state via the root
                                    // getter – Filament's Get can resolve sibling repeater data
                                    // with different depth depending on context.
                                    if (empty($allocations) && $customerId > 0) {
                                        // No allocations yet – nothing to filter.
                                        return $options;
                                    }
                                    $currentId = $get('invoice_id');
                                    $selected = collect($allocations)
                                        ->pluck('invoice_id')
                                        ->filter()
                                        ->map(fn ($v) => (int) $v)
                                        ->all();

                                    return collect($options)
                                        ->reject(fn (string $label, int $id) => in_array($id, $selected, true) && (int) $currentId !== $id)
                                        ->all();
                                })
                                ->searchable()
                                ->required()
                                ->distinct()
                                ->disableOptionWhen(function (string $value, Get $get): bool {
                                    $allocations = $get('../../invoice_allocations') ?? $get('../invoice_allocations') ?? [];
                                    $selected = collect($allocations)
                                        ->pluck('invoice_id')
                                        ->filter()
                                        ->map(fn ($v) => (string) $v)
                                        ->all();
                                    $current = (string) ($get('invoice_id') ?? '');

                                    return $value !== $current && in_array($value, $selected, true);
                                })
                                // The moment an invoice is picked, its remaining
                                // balance becomes the allocation amount — no typing,
                                // no transposed digits.
                                ->live()
                                ->afterStateUpdated(fn (Set $set, $state): mixed => $set('amount', static::invoiceBalance((int) $state))),
                            TextInput::make('amount')
                                ->numeric()->required()->minValue(0.01)->maxValue(99999999.99)->prefix('Rs.')
                                ->hint(fn (Get $get): ?string => static::invoiceBalanceHint($get('invoice_id'))),
                        ])
                        ->columns(2)
                        ->defaultItems(0)
                        ->addActionLabel('Allocate an invoice')
                        ->columnSpanFull(),
                ])->collapsed(),

            Section::make('Note')->schema([
                Textarea::make('note')->maxLength(500)->columnSpanFull(),
            ]),
        ]);
    }

    public static function table(Table $table): Table
    {
        return static::standardTableDefaults($table)
            ->recordUrl(static::standardRecordUrl())
            ->emptyStateHeading('No transactions yet')
            ->emptyStateDescription('Payments taken at the till will appear here.')

            // withTrashed() + ownership filter match the source index.
            ->modifyQueryUsing(fn (Builder $query): Builder => $query
                ->withTrashed()
                ->when(! static::canViewAll(), fn (Builder $q) => $q->where('created_by', Auth::id())))
            ->columns([
                Chip::documentNumber('transaction_number', 'Transaction #')->searchable()->sortable(),
                TextColumn::make('taken_on')->label('Date')->dateTime()->sortable(),
                TextColumn::make('customer.business_name')->label('Customer')->searchable()->placeholder('—'),
                TextColumn::make('method.name')->label('Method')->searchable()->placeholder('—'),
                TextColumn::make('amount')->money(fn () => app(CurrentContext::class)->currencyCode())->sortable(),
                TextColumn::make('status.name')
                    ->label('Status')
                    ->badge()
                    ->color(fn (Transaction $record): string => StatusLookup::colorOf(Status::CTX_TRANSACTION, $record->status?->code ?? '')),
                TextColumn::make('reference')->placeholder('—')->toggleable(),
                TextColumn::make('cheque_number')->label('Cheque #')->placeholder('—')->toggleable(isToggledHiddenByDefault: true),
                TextColumn::make('creator.name')->label('Recorded by')->placeholder('—')->visible(fn (): bool => static::canViewAll()),
                TextColumn::make('deleted_at')->label('Deleted')->dateTime()->placeholder('—')->toggleable(isToggledHiddenByDefault: true),
            ])
            ->defaultSort('taken_on', 'desc')
            ->filters([
                SelectFilter::make('status')
                    ->options(fn (): array => StatusLookup::options(Status::CTX_TRANSACTION))
                    ->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('rep')
                    ->label('Recorded by')
                    ->visible(fn (): bool => static::canViewAll())
                    ->options(fn (): array => User::query()
                        ->whereIn('id', Transaction::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),
                    )),
            ])
            ->recordActions(static::lifecycleActions());
    }

    /**
     * The approve / reject / revert / edit / delete row actions, matching the
     * source index.tsx lifecycle logic exactly: approve is only offered on
     * pending instruments, reject on pending OR confirmed instruments that
     * clear later, revert only on confirmed/rejected instruments that clear
     * later, delete anywhere a non-deleted row is editable.
     *
     * @return array<int, Action>
     */
    public static function lifecycleActions(): array
    {
        return [
            Action::make('view')
                ->url(fn (Transaction $record): string => ViewTransaction::getUrl(['record' => $record]))
                ->visible(fn (Transaction $record): bool => static::canView($record) && ! $record->trashed()),

            EditAction::make()
                ->visible(fn (Transaction $record): bool => static::canEdit($record) && ! $record->trashed()),

            Action::make('approve')
                ->label('Received')
                ->color('success')
                ->icon(Heroicon::OutlinedCheckBadge)
                ->requiresConfirmation()
                ->modalDescription('Confirm this payment has cleared. It will be added to the linked invoice(s) paid amount.')
                ->visible(fn (Transaction $record): bool => Perm::can('isms_transactions', 'approve')
                    && ! $record->trashed()
                    && $record->status?->code === Status::PENDING)
                ->action(fn (Transaction $record) => static::approve($record)),

            Action::make('reject')
                ->label('Reject')
                ->color('danger')
                ->icon(Heroicon::OutlinedXCircle)
                ->requiresConfirmation()
                ->schema([
                    TextInput::make('reason')->label('Reason')->maxLength(255),
                ])
                ->modalDescription('Mark this payment as rejected (e.g. cheque bounced). It will NOT count towards any invoice paid amount.')
                ->visible(fn (Transaction $record): bool => Perm::can('isms_transactions', 'reject')
                    && ! $record->trashed()
                    && in_array($record->status?->code, [Status::PENDING, Status::APPROVED, Status::UTILIZED], true))
                ->action(fn (Transaction $record, array $data) => static::reject($record, $data['reason'] ?? null)),

            Action::make('revert')
                ->label('Revert to pending')
                ->icon(Heroicon::OutlinedArrowPath)
                ->color('gray')
                ->requiresConfirmation()
                ->modalDescription('Move this transaction back to pending. Its amount is removed from the linked invoice(s) paid amounts until confirmed again.')
                ->visible(fn (Transaction $record): bool => Perm::can('isms_transactions', 'approve')
                    && ! $record->trashed()
                    && static::clearsLater($record)
                    && in_array($record->status?->code, [Status::APPROVED, Status::UTILIZED, Status::REJECTED], true))
                ->action(fn (Transaction $record) => static::revert($record)),

            DeleteAction::make()
                ->visible(fn (Transaction $record): bool => static::canDelete($record) && ! $record->trashed())
                ->action(function (Transaction $record): void {
                    static::destroy($record);
                }),
        ];
    }

    public static function approve(Transaction $record): void
    {
        try {
            app(PaymentService::class)->approveTransaction($record);
            Notification::make()->title('Transaction approved.')->success()->send();
        } catch (RuntimeException $e) {
            Notification::make()->title($e->getMessage())->danger()->send();
        }
    }

    public static function reject(Transaction $record, ?string $reason = null): void
    {
        try {
            app(PaymentService::class)->rejectTransaction($record, $reason);
            Notification::make()->title('Transaction rejected.')->success()->send();
        } catch (RuntimeException $e) {
            Notification::make()->title($e->getMessage())->danger()->send();
        }
    }

    public static function revert(Transaction $record): void
    {
        try {
            app(PaymentService::class)->revertTransactionToPending($record);
            Notification::make()->title('Transaction moved back to pending.')->success()->send();
        } catch (RuntimeException $e) {
            Notification::make()->title($e->getMessage())->danger()->send();
        }
    }

    /**
     * Ported from TransactionController::destroy(): soft-delete, then recompute
     * every claimed invoice's payment state with this transaction excluded.
     */
    public static function destroy(Transaction $record): void
    {
        $record->load('claims.invoice');

        $record->delete();

        foreach ($record->claims as $claim) {
            if ($claim->invoice) {
                app(PaymentService::class)->recomputeInvoicePayment($claim->invoice);
            }
        }

        Notification::make()->title('Transaction deleted and invoice balances updated.')->success()->send();
    }

    public static function isChequeMethod(mixed $paymentMethodId): bool
    {
        if (! $paymentMethodId) {
            return false;
        }

        return PaymentMethod::query()->find((int) $paymentMethodId)?->code === PaymentMethod::CHEQUE;
    }

    protected static function clearsLater(Transaction $record): bool
    {
        return in_array($record->method?->code, [PaymentMethod::CHEQUE, PaymentMethod::BANK_TRANSFER], true);
    }

    /**
     * Unpaid invoices for a customer, oldest first — the same shape as the
     * source's TransactionController::unpaidInvoices() endpoint.
     *
     * @return array<int, string>
     */
    protected static function unpaidInvoiceOptions(int $customerId): array
    {
        if ($customerId <= 0) {
            return [];
        }

        return Invoice::query()
            ->where('customer_id', $customerId)
            ->where('balance', '>', 0)
            ->orderBy('created_at')
            ->limit(100)
            ->get()
            ->mapWithKeys(fn (Invoice $i) => [$i->id => $i->invoice_number.' (Rs. '.number_format((float) $i->balance, 2).' outstanding)'])
            ->all();
    }

    /**
     * Total still owed by a customer across all unpaid invoices — the basis
     * for the amount pre-fill and the live "outstanding" hint.
     */
    public static function customerOutstanding(int $customerId): float
    {
        if ($customerId <= 0) {
            return 0.0;
        }

        return round((float) Invoice::query()
            ->where('customer_id', $customerId)
            ->where('balance', '>', 0)
            ->sum('balance'), 2);
    }

    protected static function invoiceBalance(int $invoiceId): ?float
    {
        if ($invoiceId <= 0) {
            return null;
        }

        return round((float) (Invoice::query()->find($invoiceId)?->balance ?? 0), 2);
    }

    /**
     * Live hint under the payment amount: outstanding, what the typed amount
     * leaves behind, and how much has been allocated to invoices.
     */
    protected static function amountHint(Get $get): ?string
    {
        $customerId = (int) ($get('customer_id') ?? 0);
        if ($customerId <= 0) {
            return null;
        }

        $outstanding = static::customerOutstanding($customerId);
        $parts = ['Outstanding: Rs. '.number_format($outstanding, 2)];

        $amount = (float) ($get('amount') ?? 0);
        if ($amount > 0 && $amount < $outstanding) {
            $parts[] = 'Remaining after payment: Rs. '.number_format($outstanding - $amount, 2);
        }

        $allocated = collect($get('invoice_allocations') ?? [])->sum(fn (array $row): float => (float) ($row['amount'] ?? 0));
        if ($allocated > 0) {
            $parts[] = 'Allocated: Rs. '.number_format(round($allocated, 2), 2);
        }

        return implode(' • ', $parts);
    }

    protected static function invoiceBalanceHint(?int $invoiceId): ?string
    {
        $balance = $invoiceId ? static::invoiceBalance($invoiceId) : null;

        return $balance === null ? null : 'Balance: Rs. '.number_format($balance, 2);
    }

    public static function getPages(): array
    {
        return [
            'index' => ListTransactions::route('/'),
            'create' => CreateTransaction::route('/create'),
            'view' => ViewTransaction::route('/{record}'),
            'edit' => EditTransaction::route('/{record}/edit'),
        ];
    }
}
