<?php

namespace App\Filament\Resources;

use App\Filament\Exports\DamageExporter;
use App\Filament\Resources\Concerns\AssertsBranchAccess;
use App\Filament\Resources\Concerns\HasModulePermissions;
use App\Filament\Resources\Concerns\HasStandardTableFeatures;
use App\Filament\Resources\DamageResource\Pages\CreateDamage;
use App\Filament\Resources\DamageResource\Pages\ListDamages;
use App\Filament\Support\Chip;
use App\Filament\Support\ProductSelect;
use App\Filament\Support\RelationSelect;
use App\Models\Catalog\Product;
use App\Models\Inventory\Damage;
use App\Models\Inventory\ProductSerial;
use App\Models\Inventory\StockBatch;
use App\Models\Lookups\DamageReason;
use App\Models\Lookups\MovementType;
use App\Models\Lookups\Status;
use App\Models\Scopes\BranchScope;
use App\Services\AuditLog;
use App\Services\BatchService;
use App\Services\CurrentContext;
use App\Services\DamageService;
use App\Services\SerialService;
use App\Services\StockMovementService;
use App\Support\Quantity;
use Filament\Actions\DeleteAction;
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\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;
use Illuminate\Support\Facades\DB;
use Throwable;

/**
 * Ported from App\Http\Controllers\Isms\DamageController + DamageService
 * (docs/migration-plan.md Phase 4). Manual write-off only — auto damage from
 * a return (source_type = "return_item") is created by ReturnService in
 * Phase 6 and, matching the source, cannot be deleted from here ("This
 * damage originated from a customer return. Delete the return instead.").
 * Serial-tracked products create one Damage row per selected serial
 * (DamageService::recordManual()'s own loop, unchanged) and quantity_damaged
 * is implicitly 1 per serial — the form's quantity field hides once serials
 * are picked to match that.
 */
class DamageResource extends Resource
{
    use AssertsBranchAccess;
    use HasModulePermissions;
    use HasStandardTableFeatures;

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

    protected static string $permissionModuleKey = 'isms_stock_damages';

    protected static string $exporter = DamageExporter::class;

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

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

    protected static ?string $navigationLabel = 'Damages';

    protected static ?int $navigationSort = 30;

    public static function form(Schema $schema): Schema
    {
        return $schema->components([
            Section::make()->schema([
                RelationSelect::make(
                    name: 'branch_id',
                    relationship: 'branch',
                    titleAttribute: 'name',
                    modifyQueryUsing: fn ($query) => $query->where('is_active', true),
                )->label('Branch')->required(),
                ProductSelect::make(
                    modifyQueryUsing: fn ($query) => $query->where('is_active', true),
                )->label('Product')->required()->live(),
                RelationSelect::make(
                    name: 'damage_reason_id',
                    relationship: 'reason',
                    titleAttribute: 'name',
                )->label('Reason')->required(),
                RelationSelect::standalone(
                    name: 'stock_batch_id',
                    modelClass: StockBatch::class,
                    titleAttribute: 'internal_batch_number',
                    // Scoped to the form's own branch pick, not the acting
                    // user's ambient context branch (see adjustment resource).
                    // notDeactivated(), not active(): writing off expired or
                    // recalled stock is exactly what this form is for, but a
                    // lot an operator has parked must not be offered at all.
                    modifyQueryUsing: fn ($query, Get $get) => $query
                        ->withoutGlobalScope(BranchScope::class)
                        ->where('product_id', $get('product_id'))
                        ->notDeactivated()
                        ->when(filled($get('branch_id')), fn ($q) => $q->where('branch_id', (int) $get('branch_id'))),
                )
                    ->label('Lot')
                    ->live()
                    // The picker already hides these; the rule catches a stale
                    // form whose options were loaded before the deactivation.
                    ->rule(fn (): \Closure => function (string $attribute, $value, \Closure $fail): void {
                        if (filled($value) && StockBatch::withoutGlobalScope(BranchScope::class)->find($value)?->isInactive()) {
                            $fail('This lot is deactivated. Reactivate it from Stock Batches before writing stock off it.');
                        }
                    })
                    // Every product has lots now (a GRN receipt prices each
                    // one), so a manual write-off names the lot it hits — that
                    // keeps Î£ lot.available_quantity reconciled with
                    // branch_stock. When serials are picked instead, the
                    // service derives the lot from each serial.
                    ->visible(fn (Get $get): bool => filled($get('product_id')))
                    ->required(fn (Get $get): bool => filled($get('product_id'))
                        && blank($get('serial_ids'))),
                Select::make('serial_ids')
                    ->label('Serial Numbers')
                    ->multiple()
                    ->live()
                    ->options(fn (Get $get) => ProductSerial::where('product_id', $get('product_id'))
                        ->when(filled($get('branch_id')), fn ($q) => $q->withoutGlobalScope(BranchScope::class)->where('branch_id', (int) $get('branch_id')))
                        ->where('status_id', Status::idOf(Status::CTX_PRODUCT_SERIAL, Status::SERIAL_AVAILABLE))
                        ->pluck('serial_number', 'id'))
                    ->visible(fn (Get $get): bool => filled($get('product_id'))
                        && (Product::find($get('product_id'))?->hasAvailableSerials(
                            filled($get('branch_id')) ? (int) $get('branch_id') : null,
                        ) ?? false)),
                TextInput::make('quantity_damaged')
                    ->label('Quantity')
                    ->numeric()
                    ->required()
                    ->minValue(0.01)
                    ->maxValue(1000000)
                    ->suffix(fn (Get $get): ?string => Quantity::unitLabel(Product::find($get('product_id'))?->baseUnit))
                    ->visible(fn (Get $get): bool => blank($get('serial_ids'))),
                TextInput::make('unit_cost')->numeric()->minValue(0)->maxValue(99999999.99)->prefix('Rs.'),
                Textarea::make('note')->maxLength(500)->columnSpanFull(),
            ])->columns(2),
        ]);
    }

    public static function table(Table $table): Table
    {
        return static::standardTableDefaults($table)
            ->recordUrl(static::standardRecordUrl())
            ->emptyStateHeading('No damage records yet')
            ->emptyStateDescription('Write-offs recorded in inventory will appear here.')
            ->columns([
                Chip::documentNumber('damage_number', 'Damage #')->searchable()->sortable(),
                TextColumn::make('branch.name'),
                TextColumn::make('product.name')->searchable(),
                TextColumn::make('reason.name'),
                TextColumn::make('quantity_damaged')
                    ->label('Quantity')
                    ->formatStateUsing(fn ($state, Damage $record): string => Quantity::format($state, $record->unit)),
                TextColumn::make('total_damage_cost')->money(fn () => app(CurrentContext::class)->currencyCode()),
                TextColumn::make('created_at')->dateTime()->sortable(),
            ])
            ->defaultSort('created_at', 'desc')
            ->filters([
                Filter::make('damage_number')
                    ->schema([TextInput::make('value')->label('Damage #')])
                    ->query(fn (Builder $query, array $data): Builder => $query->when(
                        $data['value'] ?? null,
                        fn (Builder $q, $value) => $q->where('damage_number', 'like', "%{$value}%"),
                    )),
                SelectFilter::make('branch_id')->relationship('branch', 'name'),
                Filter::make('product')
                    ->schema([TextInput::make('value')->label('Product')])
                    ->query(fn (Builder $query, array $data): Builder => $query->when(
                        $data['value'] ?? null,
                        fn (Builder $q, $value) => $q->whereHas('product', fn ($qq) => $qq->where('name', 'like', "%{$value}%")),
                    )),
                SelectFilter::make('damage_reason_id')->label('Reason')->relationship('reason', 'name'),
            ])
            ->filtersLayout(FiltersLayout::AboveContentCollapsible)
            ->filtersFormColumns(2)
            ->headerActions(static::standardExportActions())
            ->toolbarActions(static::standardExportBulkActions())
            ->recordActions([
                DeleteAction::make()
                    ->before(function (Damage $record, DeleteAction $action): void {
                        if ($record->source_type === 'return_item') {
                            Notification::make()->title('This damage originated from a customer return. Delete the return instead.')->danger()->send();
                            $action->cancel();

                            return;
                        }

                        try {
                            static::reverse($record);
                        } catch (Throwable $e) {
                            Notification::make()->title($e->getMessage())->danger()->send();
                            $action->cancel();
                        }
                    }),
            ]);
    }

    public static function getPages(): array
    {
        return [
            'index' => ListDamages::route('/'),
            'create' => CreateDamage::route('/create'),
        ];
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public static function apply(array $data): Damage
    {
        $reasonCode = DamageReason::find($data['damage_reason_id'])?->code;

        return app(DamageService::class)->recordManual([
            'branch_id' => (int) $data['branch_id'],
            'product_id' => (int) $data['product_id'],
            'unit_id' => isset($data['unit_id']) ? (int) $data['unit_id'] : null,
            'damage_reason_code' => $reasonCode,
            'quantity' => isset($data['quantity_damaged']) ? (float) $data['quantity_damaged'] : 0.0,
            'unit_cost' => isset($data['unit_cost']) ? (float) $data['unit_cost'] : null,
            'note' => $data['note'] ?? null,
            'stock_batch_id' => isset($data['stock_batch_id']) ? (int) $data['stock_batch_id'] : null,
            'serial_ids' => $data['serial_ids'] ?? null,
        ]);
    }

    /**
     * Ported verbatim from DamageController::destroy().
     */
    public static function reverse(Damage $damage): void
    {
        DB::transaction(function () use ($damage) {
            app(StockMovementService::class)->record(
                branchId: (int) $damage->branch_id,
                productId: (int) $damage->product_id,
                movementTypeCode: MovementType::DAMAGE_REVERSAL,
                quantityChange: (float) $damage->quantity_damaged,
                unitCost: isset($damage->unit_cost) ? (float) $damage->unit_cost : null,
                referenceType: $damage,
                referenceNumber: $damage->damage_number,
                note: 'Damage deletion reversal',
                stockBatchId: $damage->stock_batch_id,
            );

            if ($damage->stock_batch_id) {
                app(BatchService::class)->reverseDamage($damage->stockBatch, (float) $damage->quantity_damaged);
            }
            if ($damage->product_serial_id) {
                app(SerialService::class)->restoreToStock($damage->serial, (int) $damage->branch_id);
            }

            AuditLog::record('damage.deleted', $damage, [
                'damage_number' => $damage->damage_number,
                'quantity_reversed' => (string) $damage->quantity_damaged,
            ]);
        });
    }
}
