<?php

namespace App\Filament\Resources;

use App\Filament\Exports\ReturnGrnExporter;
use App\Filament\Resources\Concerns\AssertsBranchAccess;
use App\Filament\Resources\Concerns\HasModulePermissions;
use App\Filament\Resources\Concerns\HasStandardTableFeatures;
use App\Filament\Resources\ReturnGrnResource\Pages\CreateReturnGrn;
use App\Filament\Resources\ReturnGrnResource\Pages\ListReturnGrns;
use App\Filament\Resources\ReturnGrnResource\Pages\ViewReturnGrn;
use App\Filament\Support\Chip;
use App\Filament\Support\Perm;
use App\Filament\Support\ProductSelect;
use App\Filament\Support\RelationSelect;
use App\Filament\Support\StatusLookup;
use App\Models\Catalog\Product;
use App\Models\Inventory\GrnItem;
use App\Models\Inventory\ProductSerial;
use App\Models\Inventory\ReturnGrn;
use App\Models\Inventory\StockBatch;
use App\Models\Lookups\GrnStatus;
use App\Models\Scopes\BranchScope;
use App\Services\CurrentContext;
use App\Services\ReturnGrnService;
use App\Support\Quantity;
use App\Support\SerialInput;
use Filament\Actions\Action;
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\Enums\FiltersLayout;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;

/**
 * Return GRN ("Return to Supplier"): goods going back to a supplier, the
 * mirror of a GRN — same draft→confirm→(optionally deleted) lifecycle:
 *
 *  - create() only ever produces a DRAFT return GRN with its line items —
 *    no stock effect yet.
 *  - confirm() (ReturnGrnService::confirm) is the only thing that posts
 *    `return_out` stock movements, consumes batch quantity (the line's own
 *    batch when sourced from a GRN item, otherwise FEFO), flags returning
 *    serials as returned_to_supplier, and credits the supplier ledger.
 *  - delete() is a STATUS change (return_grn_status_id → DELETED), not an
 *    Eloquent delete/soft-delete — ReturnGrn has no SoftDeletes trait; the
 *    record stays visible. If confirmed, deletion reverses every movement
 *    (`return_out_reversal`), restocks batches, restores serials, and debits
 *    the supplier back. No EditRecord page, matching GRN (no update route).
 *
 * Each line can optionally be traced back to the GRN item the goods arrived
 * on: picking one pins the batch to consume at confirm, seeds the cost, and
 * for serial-tracked products pre-fills the serial numbers of that receipt.
 */
class ReturnGrnResource extends Resource
{
    use AssertsBranchAccess;
    use HasModulePermissions;
    use HasStandardTableFeatures;

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

    protected static string $permissionModuleKey = 'isms_return_grn';

    protected static string $exporter = ReturnGrnExporter::class;

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

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

    protected static ?string $navigationLabel = 'Return GRN';

    protected static ?int $navigationSort = 5;

    public static function form(Schema $schema): Schema
    {
        return $schema->components([
            Section::make('Details')->schema([
                RelationSelect::make(
                    name: 'branch_id',
                    relationship: 'branch',
                    titleAttribute: 'name',
                    modifyQueryUsing: fn ($query) => $query->where('is_active', true),
                )->label('Branch')->required(),
                RelationSelect::make(
                    name: 'supplier_id',
                    relationship: 'supplier',
                    titleAttribute: 'name',
                    createPermission: 'isms_suppliers.create',
                )
                    ->label('Supplier')
                    // Optional: a return can be filed against a branch without
                    // naming a supplier (the column is nullable, and the credit
                    // leg only fires when a supplier is set).
                    ->visible(fn (): bool => Perm::can('isms_suppliers', 'access')),
                TextInput::make('reference')
                    ->maxLength(100)
                    ->helperText('Your own reference for this return (supplier note number, etc.).'),
                TextInput::make('reason')
                    ->label('Reason')
                    ->maxLength(500)
                    ->placeholder('e.g. Defective batch, wrong items, excess stock…')
                    ->columnSpanFull(),
                Textarea::make('note')->maxLength(500)->columnSpanFull(),
            ])->columns(2),

            Section::make('Items')->schema([
                Repeater::make('items')
                    ->schema([
                        ProductSelect::standalone(
                            modifyQueryUsing: fn ($query) => $query->where('is_active', true),
                        )
                            ->label('Product')
                            ->required()
                            ->columnSpanFull()
                            ->live()
                            // Seed the cost from the product's most recent lot,
                            // so the operator edits a real number rather than
                            // guessing. Blank when never received.
                            ->afterStateUpdated(function (Set $set, $state): void {
                                if (! $state) {
                                    return;
                                }

                                $cost = StockBatch::query()
                                    ->where('product_id', (int) $state)
                                    ->notDeactivated()
                                    ->whereNotNull('batch_cost')
                                    ->orderByDesc('created_at')
                                    ->value('batch_cost');

                                if ($cost !== null) {
                                    $set('unit_cost', (float) $cost);
                                }
                            }),
                        // Optional: pin this line to the exact GRN line the
                        // goods arrived on. Choosing one seeds the batch and
                        // cost from that receipt, and pre-fills serials for
                        // serial-tracked products — so a return of received
                        // goods is traceable all the way back to the receipt.
                        Select::make('source_grn_item_id')
                            ->label('Return against GRN item')
                            ->helperText('Optional — picks the batch, cost and serials from the receipt this stock arrived on.')
                            ->columnSpanFull()
                            ->live()
                            ->options(function (Get $get): array {
                                return GrnItem::query()
                                    ->whereHas('grn', fn (Builder $q) => $q
                                        ->where('grn_status_id', GrnStatus::CONFIRMED)
                                        ->when(
                                            (int) ($get('../../supplier_id') ?? 0) > 0,
                                            fn (Builder $qq, $enabled) => $qq->where('supplier_id', (int) $get('../../supplier_id')),
                                        ))
                                    ->when((int) ($get('product_id') ?? 0) > 0, fn (Builder $q, $enabled) => $q->where('product_id', (int) $get('product_id')))
                                    ->with(['grn:id,grn_number', 'product:id,name'])
                                    ->limit(100)
                                    ->get()
                                    ->mapWithKeys(fn (GrnItem $item): array => [
                                        $item->id => sprintf(
                                            '%s · %s Ã— %s',
                                            $item->grn?->grn_number ?? "GRN#{$item->grn_id}",
                                            $item->product?->name ?? "Product#{$item->product_id}",
                                            rtrim(rtrim(number_format((float) $item->quantity, 4, '.', ''), '0'), '.'),
                                        ),
                                    ])
                                    ->all();
                            })
                            ->afterStateUpdated(function (Set $set, $state): void {
                                if (! $state) {
                                    return;
                                }
                                $item = GrnItem::find($state);
                                if (! $item) {
                                    return;
                                }
                                $set('stock_batch_id', $item->stock_batch_id);
                                if ($item->unit_cost !== null) {
                                    $set('unit_cost', (float) $item->unit_cost);
                                }
                                if (! empty($item->serial_numbers)) {
                                    $set('serial_numbers', SerialInput::numbers($item->serial_numbers));
                                }
                            }),
                        TextInput::make('quantity')
                            ->numeric()->required()->minValue(0.01)
                            ->suffix(fn (Get $get): ?string => Quantity::unitLabel(Product::find($get('product_id'))?->baseUnit)),
                        TextInput::make('unit_cost')
                            ->label('Cost at return')
                            ->numeric()
                            ->minValue(0)
                            ->prefix('Rs.')
                            ->helperText('Used for the supplier ledger entry; blank lines contribute no value.'),
                        // Serial-tracked lines return as a picker of the
                        // serials actually on file (available + at the form's
                        // branch), storing serial NUMBERS — the same shape the
                        // service reads and the source-GRN prefill writes.
                        Select::make('serial_numbers')
                            ->label('Serial numbers')
                            ->multiple()
                            ->live()
                            ->options(fn (Get $get) => ProductSerial::query()
                                ->where('product_id', (int) $get('product_id'))
                                ->when(
                                    (int) ($get('../../branch_id') ?? 0) > 0,
                                    fn ($q, $enabled) => $q->withoutGlobalScope(BranchScope::class)->where('branch_id', (int) $get('../../branch_id')),
                                )
                                ->available()
                                ->pluck('serial_number', 'serial_number')
                                ->all())
                            ->visible(fn (Get $get): bool => filled($get('product_id'))
                                && (Product::find($get('product_id'))?->hasAvailableSerials(
                                    (int) ($get('../../branch_id') ?? 0) > 0 ? (int) $get('../../branch_id') : null,
                                ) ?? false))
                            ->columnSpanFull(),
                        // The lot the returned goods came off. Picking the
                        // source GRN item above fills this automatically; the
                        // picker is here so a line can name its lot without
                        // tracing back to a receipt. The return always names
                        // its lot — confirmation decrements that lot's
                        // available quantity, serials included.
                        Select::make('stock_batch_id')
                            ->label('Lot')
                            ->helperText('Which consignment the returned goods came from (filled automatically when a GRN item is picked).')
                            ->live()
                            ->options(function (Get $get): array {
                                if (! filled($get('product_id'))) {
                                    return [];
                                }

                                return StockBatch::query()
                                    ->withoutGlobalScope(BranchScope::class)
                                    ->where('product_id', (int) $get('product_id'))
                                    ->when(
                                        (int) ($get('../../branch_id') ?? 0) > 0,
                                        fn ($q, $branchId) => $q->where('branch_id', $branchId),
                                    )
                                    ->active()
                                    ->orderByDesc('id')
                                    ->get(['id', 'internal_batch_number', 'sale_price', 'available_quantity'])
                                    ->mapWithKeys(fn (StockBatch $lot): array => [
                                        $lot->id => sprintf(
                                            '%s — %s Ã— %s',
                                            $lot->internal_batch_number,
                                            rtrim(rtrim(number_format((float) $lot->available_quantity, 4, '.', ''), '0'), '.'),
                                            $lot->sale_price !== null ? 'Rs. '.number_format((float) $lot->sale_price, 2) : 'unpriced',
                                        ),
                                    ])
                                    ->all();
                            })
                            ->required(fn (Get $get): bool => filled($get('product_id')))
                            ->afterStateUpdated(function (Get $get, Set $set, $state): void {
                                if (! $state) {
                                    return;
                                }
                                $cost = StockBatch::query()
                                    ->withoutGlobalScope(BranchScope::class)
                                    ->whereKey((int) $state)
                                    ->value('batch_cost');
                                if ($cost !== null) {
                                    $set('unit_cost', (float) $cost);
                                }
                            }),
                    ])
                    ->columns(2)
                    ->minItems(1)
                    ->addActionLabel('Add item')
                    ->columnSpanFull(),
            ]),
        ]);
    }

    public static function table(Table $table): Table
    {
        return static::standardTableDefaults($table)
            ->recordUrl(static::standardRecordUrl())
            ->emptyStateHeading('No return GRNs yet')
            ->emptyStateDescription('Goods sent back to a supplier will appear here.')
            ->columns([
                Chip::documentNumber('return_grn_number', 'Return GRN #')->searchable()->sortable(),
                TextColumn::make('branch.name'),
                TextColumn::make('supplier.name')->placeholder('—'),
                TextColumn::make('reference')->placeholder('—'),
                TextColumn::make('items_count')->label('Items')->counts('items'),
                TextColumn::make('total_cost')->label('Total')->money(fn () => app(CurrentContext::class)->currencyCode())->sortable(),
                TextColumn::make('returnGrnStatus.name')
                    ->label('Status')
                    ->badge()
                    ->color(fn (ReturnGrn $record): string => StatusLookup::colorOfStatus($record->returnGrnStatus)),
                TextColumn::make('created_at')->dateTime()->sortable(),
            ])
            ->defaultSort('created_at', 'desc')
            ->filters([
                Filter::make('return_grn_number')
                    ->schema([TextInput::make('value')->label('Return GRN #')])
                    ->query(fn (Builder $query, array $data): Builder => $query->when(
                        $data['value'] ?? null,
                        fn (Builder $q, $value) => $q->where('return_grn_number', 'like', "%{$value}%"),
                    )),
                SelectFilter::make('branch_id')->relationship('branch', 'name'),
                SelectFilter::make('supplier_id')->relationship('supplier', 'name'),
                SelectFilter::make('return_grn_status_id')->label('Status')->relationship('returnGrnStatus', 'name'),
            ])
            ->filtersLayout(FiltersLayout::AboveContentCollapsible)
            ->filtersFormColumns(2)
            ->headerActions(static::standardExportActions())
            ->toolbarActions(static::standardExportBulkActions())
            ->recordActions(static::transitionActions());
    }

    /**
     * Confirm / delete transitions, shared by the table row actions and the
     * view page's header actions so both behave identically.
     *
     * @return array<int, Action>
     */
    public static function transitionActions(): array
    {
        return [
            Action::make('view')
                ->url(fn (ReturnGrn $record): string => ViewReturnGrn::getUrl(['record' => $record]))
                ->visible(fn (ReturnGrn $record): bool => static::canView($record)),
            Action::make('confirm')
                ->label('Confirm return')
                ->visible(fn (ReturnGrn $record): bool => Perm::can('isms_return_grn', 'confirm') && $record->isDraft())
                ->requiresConfirmation()
                ->action(function (ReturnGrn $record): void {
                    try {
                        app(ReturnGrnService::class)->confirm($record, auth()->id());
                        Notification::make()->title('Return GRN confirmed. Stock has been returned to the supplier.')->success()->send();
                    } catch (\Throwable $e) {
                        Notification::make()->title('Failed to confirm return GRN: '.$e->getMessage())->danger()->send();
                    }
                }),
            Action::make('delete')
                ->label('Delete')
                ->color('danger')
                ->visible(fn (ReturnGrn $record): bool => Perm::can('isms_return_grn', 'delete') && ! $record->isDeleted())
                ->requiresConfirmation()
                ->action(function (ReturnGrn $record): void {
                    try {
                        $wasConfirmed = $record->isConfirmed();
                        app(ReturnGrnService::class)->delete($record, auth()->id());
                        Notification::make()
                            ->title($wasConfirmed
                                ? "Return GRN {$record->return_grn_number} deleted. Stock quantities have been reversed."
                                : "Draft return GRN {$record->return_grn_number} deleted.")
                            ->success()
                            ->send();
                    } catch (\Throwable $e) {
                        Notification::make()->title('Failed to delete return GRN: '.$e->getMessage())->danger()->send();
                    }
                }),
        ];
    }

    public static function getPages(): array
    {
        return [
            'index' => ListReturnGrns::route('/'),
            'create' => CreateReturnGrn::route('/create'),
            'view' => ViewReturnGrn::route('/{record}'),
        ];
    }
}
