<?php

namespace App\Filament\Resources;

use App\Filament\Resources\Concerns\AssertsBranchAccess;
use App\Filament\Resources\Concerns\HasModulePermissions;
use App\Filament\Resources\Concerns\HasStandardTableFeatures;
use App\Filament\Resources\StockAdjustmentResource\Pages\CreateStockAdjustment;
use App\Filament\Resources\StockAdjustmentResource\Pages\ListStockAdjustments;
use App\Filament\Support\ProductSelect;
use App\Filament\Support\RelationSelect;
use App\Models\BranchStock;
use App\Models\Catalog\Product;
use App\Models\Inventory\ProductSerial;
use App\Models\Inventory\StockAdjustment;
use App\Models\Inventory\StockBatch;
use App\Models\Lookups\MovementType;
use App\Models\Scopes\BranchScope;
use App\Services\AuditLog;
use App\Services\BatchService;
use App\Services\StockMovementService;
use App\Support\Quantity;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
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\Filter;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

/**
 * Ported from App\Http\Controllers\Isms\StockAdjustmentController
 * (docs/migration-plan.md Phase 4). No edit page — an adjustment is a
 * point-in-time snapshot (before_stock/new_stock/diff), matching the source
 * (only create + delete routes exist). Deleting reverses the diff via
 * MovementType::ADJUSTMENT_REVERSAL — also missing from LookupSeeder until
 * this phase (see Decisions Log correction: the Phase 3 fix grepped only
 * app/Services/*.php and missed this direct controller-level reference).
 * Serial-tracked products are rejected outright; every other product
 * requires a lot (stock_batch_id), and "new_stock" means the lot's
 * available_quantity, not the branch total — stock is corrected lot by lot.
 */
class StockAdjustmentResource extends Resource
{
    use AssertsBranchAccess;
    use HasModulePermissions;
    use HasStandardTableFeatures;

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

    protected static string $permissionModuleKey = 'isms_stock_adjustment';

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

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

    protected static ?string $navigationLabel = 'Stock Adjustments';

    protected static ?int $navigationSort = 20;

    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()
                    ->live()
                    ->afterStateUpdated(fn (Get $get, Set $set) => $set('stock_batch_id', null)),
                ProductSelect::make(
                    modifyQueryUsing: fn ($query) => $query->where('is_active', true),
                )
                    ->label('Product')
                    ->required()
                    ->live()
                    ->afterStateUpdated(fn (Get $get, Set $set) => $set('stock_batch_id', null)),
                RelationSelect::make(
                    name: 'adjustment_reason_id',
                    relationship: 'reason',
                    titleAttribute: 'name',
                )->label('Reason')->required(),
                RelationSelect::standalone(
                    name: 'stock_batch_id',
                    modelClass: StockBatch::class,
                    titleAttribute: 'internal_batch_number',
                    // Batches are scoped to the form's own branch pick, not the
                    // acting user's ambient context branch — the two routinely
                    // differ (multi-branch admin adjusting another branch), and
                    // BranchScope would otherwise hide every batch there and
                    // pair a context-branch batch with the picked branch.
                    // notDeactivated(), not active(): correcting the count on
                    // an expired or recalled lot is legitimate work, 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()
                    // Every product has lots now (a GRN receipt prices each
                    // one), so every adjustment names the lot being corrected —
                    // that is what keeps Σ lot.available_quantity reconciled
                    // with branch_stock. Lots that carry serials are rejected
                    // wholesale by the rule below.
                    ->rule(function (Get $get): \Closure {
                        return function (string $attribute, $value, \Closure $fail): void {
                            if (filled($value) && ProductSerial::where('stock_batch_id', $value)->available()->exists()) {
                                $fail('This lot carries serials — bulk-adjusting it would desync the serial ledger. Mark individual serials lost/damaged/written-off from the Serial Numbers page instead.');
                            }
                        };
                    })
                    ->visible(fn (Get $get): bool => filled($get('product_id')))
                    ->required(fn (Get $get): bool => filled($get('product_id'))),
                TextInput::make('new_stock')
                    ->label('New Stock')
                    ->numeric()
                    ->required()
                    ->minValue(0)
                    ->maxValue(1000000)
                    ->suffix(fn (Get $get): ?string => Quantity::unitLabel(Product::find($get('product_id'))?->baseUnit)),
                TextEntry::make('current_stock')
                    ->label('Current Active Stock')
                    ->state(function (Get $get): string {
                        $productId = $get('product_id');
                        $branchId = $get('branch_id');
                        $batchId = $get('stock_batch_id');

                        $product = filled($productId) ? Product::with('baseUnit')->find((int) $productId) : null;
                        if (! $product) {
                            return 'Select a product to see its current active stock.';
                        }

                        $quantity = filled($batchId)
                            ? (float) (static::findBatch((int) $batchId)?->available_quantity ?? 0)
                            : (filled($branchId) ? BranchStock::quantityFor((int) $branchId, (int) $productId) : 0.0);

                        return Quantity::format($quantity, $product->baseUnit);
                    })
                    ->helperText('The count New Stock is measured against — the selected lot\'s available quantity (every adjustment corrects one lot).')
                    ->live()
                    ->columnSpanFull(),
                Textarea::make('note')->maxLength(500)->columnSpanFull(),
            ])->columns(2),
        ]);
    }

    public static function table(Table $table): Table
    {
        return static::standardTableDefaults($table)
            ->recordUrl(static::standardRecordUrl())
            ->emptyStateHeading('No stock adjustments yet')
            ->emptyStateDescription('Count corrections and adjustments will appear here.')
            ->columns([
                TextColumn::make('branch.name'),
                TextColumn::make('product.name')->searchable(),
                TextColumn::make('reason.name'),
                TextColumn::make('before_stock')
                    ->formatStateUsing(fn ($state, StockAdjustment $record): string => Quantity::format($state, $record->unit)),
                TextColumn::make('new_stock')
                    ->formatStateUsing(fn ($state, StockAdjustment $record): string => Quantity::format($state, $record->unit)),
                TextColumn::make('diff')
                    ->formatStateUsing(fn ($state, StockAdjustment $record): string => Quantity::format($state, $record->unit))
                    ->color(fn (StockAdjustment $record): string => (float) $record->diff >= 0 ? 'success' : 'danger'),
                TextColumn::make('created_at')->dateTime()->sortable(),
            ])
            ->defaultSort('created_at', 'desc')
            ->filters([
                SelectFilter::make('branch_id')->relationship('branch', 'name'),
                Filter::make('date_from')->schema([DatePicker::make('date_from')])
                    ->query(fn (Builder $query, array $data): Builder => $query->when($data['date_from'] ?? null, fn ($q, $d) => $q->whereDate('created_at', '>=', $d))),
                Filter::make('date_to')->schema([DatePicker::make('date_to')])
                    ->query(fn (Builder $query, array $data): Builder => $query->when($data['date_to'] ?? null, fn ($q, $d) => $q->whereDate('created_at', '<=', $d))),
            ])
            ->recordActions([
                DeleteAction::make()
                    ->before(function (StockAdjustment $record, DeleteAction $action): void {
                        try {
                            static::reverse($record);
                        } catch (\Throwable $e) {
                            Notification::make()->title($e->getMessage())->danger()->send();
                            $action->cancel();
                        }
                    }),
            ]);
    }

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

    /**
     * Ported verbatim from StockAdjustmentController::store()'s core logic —
     * computes before_stock/diff (batch available_quantity), creates the
     * snapshot row, and posts the stock movement.
     *
     * @param  array<string, mixed>  $data
     */
    public static function apply(array $data): StockAdjustment
    {
        return DB::transaction(function () use ($data) {
            $branchId = (int) $data['branch_id'];
            $productId = (int) $data['product_id'];
            $newStock = (float) $data['new_stock'];
            $stockBatchId = isset($data['stock_batch_id']) && filled($data['stock_batch_id']) ? (int) $data['stock_batch_id'] : null;

            if (! $stockBatchId) {
                throw ValidationException::withMessages([
                    'stock_batch_id' => 'Select which lot you are adjusting — stock is corrected lot by lot.',
                ]);
            }

            $batch = $stockBatchId ? static::findBatch($stockBatchId) : null;

            if ($stockBatchId && ! $batch) {
                throw ValidationException::withMessages([
                    'stock_batch_id' => 'The selected batch could not be found.',
                ]);
            }

            if ($batch && (int) $batch->branch_id !== $branchId) {
                throw ValidationException::withMessages([
                    'stock_batch_id' => 'The selected batch does not belong to the chosen branch.',
                ]);
            }

            if ($batch && (int) $batch->product_id !== $productId) {
                throw ValidationException::withMessages([
                    'stock_batch_id' => 'The selected batch does not belong to the chosen product.',
                ]);
            }

            // The picker already hides these; this catches a stale form whose
            // options were loaded before the lot was deactivated.
            if ($batch && $batch->isInactive()) {
                throw ValidationException::withMessages([
                    'stock_batch_id' => 'This lot is deactivated. Reactivate it from Stock Batches before correcting its count.',
                ]);
            }

            $currentStock = $batch
                ? (float) $batch->available_quantity
                : BranchStock::quantityFor($branchId, $productId);

            $diff = $newStock - $currentStock;

            $adjustment = StockAdjustment::create([
                'branch_id' => $branchId,
                'product_id' => $productId,
                'unit_id' => isset($data['unit_id']) ? (int) $data['unit_id'] : null,
                'adjustment_reason_id' => $data['adjustment_reason_id'],
                'stock_batch_id' => $stockBatchId,
                'before_stock' => $currentStock,
                'new_stock' => $newStock,
                'diff' => $diff,
                'note' => $data['note'] ?? null,
            ]);

            if ($diff !== 0.0) {
                app(StockMovementService::class)->record(
                    branchId: $branchId,
                    productId: $productId,
                    movementTypeCode: MovementType::ADJUSTMENT,
                    quantityChange: $diff,
                    referenceType: $adjustment,
                    note: $data['note'] ?? null,
                    stockBatchId: $stockBatchId,
                );

                if ($batch) {
                    app(BatchService::class)->adjust($batch, $newStock);
                }
            }

            return $adjustment;
        });
    }

    /**
     * Ported verbatim from StockAdjustmentController::destroy().
     */
    public static function reverse(StockAdjustment $adjustment): void
    {
        DB::transaction(function () use ($adjustment) {
            $diff = (float) $adjustment->diff;
            if ($diff !== 0.0) {
                app(StockMovementService::class)->record(
                    branchId: (int) $adjustment->branch_id,
                    productId: (int) $adjustment->product_id,
                    movementTypeCode: MovementType::ADJUSTMENT_REVERSAL,
                    quantityChange: -$diff,
                    referenceType: $adjustment,
                    note: 'Adjustment deletion reversal',
                    stockBatchId: $adjustment->stock_batch_id,
                );

                if ($adjustment->stock_batch_id) {
                    $batch = static::findBatch((int) $adjustment->stock_batch_id);
                    if ($batch) {
                        app(BatchService::class)->adjust($batch, (float) $adjustment->before_stock);
                    }
                }
            }

            AuditLog::record('stock_adjustment.deleted', $adjustment, [
                'branch_id' => $adjustment->branch_id,
                'product_id' => $adjustment->product_id,
                'diff_reversed' => (string) $diff,
            ]);
        });
    }

    /**
     * Batches are keyed by an explicit business parameter (the form's branch
     * pick), so the ambient BranchScope must be bypassed — see the batch
     * picker in form() for the same reasoning.
     */
    private static function findBatch(int $id): ?StockBatch
    {
        return StockBatch::query()
            ->withoutGlobalScope(BranchScope::class)
            ->find($id);
    }
}
