<?php

namespace App\Filament\Resources;

use App\Filament\Resources\Concerns\HasModulePermissions;
use App\Filament\Resources\Concerns\HasStandardTableFeatures;
use App\Filament\Resources\StockTransferResource\Pages\CreateStockTransfer;
use App\Filament\Resources\StockTransferResource\Pages\ListStockTransfers;
use App\Filament\Support\ProductSelect;
use App\Filament\Support\RelationSelect;
use App\Models\Catalog\Product;
use App\Models\Inventory\ProductSerial;
use App\Models\Inventory\StockBatch;
use App\Models\Inventory\StockTransfer;
use App\Models\Lookups\MovementType;
use App\Models\Lookups\Status;
use App\Models\Scopes\AccessibleBranchScope;
use App\Models\Scopes\BranchScope;
use App\Services\AuditLog;
use App\Services\BatchService;
use App\Services\CurrentContext;
use App\Services\SerialService;
use App\Services\StockMovementService;
use App\Support\Quantity;
use App\Support\SerialInput;
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\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Throwable;

/**
 * Ported from App\Http\Controllers\Isms\StockTransferController
 * (docs/migration-plan.md Phase 4) — logic was inline in the controller in
 * the source (no dedicated service), so apply()/reverse() here reproduce it
 * verbatim rather than delegating to a service that doesn't exist. Two
 * StockMovementService::record() calls per transfer (TRANSFER_OUT at the
 * source, TRANSFER_IN at the destination); serial-tracked quantity is
 * derived from count(serial_ids), not entered; the transfer number
 * (TRF-{year}-{id:5}) is derived from the row's own id after insert, exactly
 * as source. No edit page — matches the source (create + delete only).
 */
class StockTransferResource extends Resource
{
    use HasModulePermissions;
    use HasStandardTableFeatures;

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

    protected static string $permissionModuleKey = 'isms_stock_transfer';

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

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

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

    protected static ?int $navigationSort = 10;

    public static function form(Schema $schema): Schema
    {
        return $schema->components([
            Section::make()->schema([
                RelationSelect::make(
                    name: 'from_branch_id',
                    relationship: 'fromBranch',
                    titleAttribute: 'name',
                    modifyQueryUsing: fn ($query) => $query->where('is_active', true),
                )->label('From Branch')->required(),
                RelationSelect::make(
                    name: 'to_branch_id',
                    relationship: 'toBranch',
                    titleAttribute: 'name',
                    // The destination is deliberately NOT restricted to the
                    // acting user's accessible branches: transfers exist to
                    // move stock out of the user's branch into any other, and
                    // the source-side assertion (assertSourceBranchAccessible)
                    // is what protects the branch the stock actually leaves.
                    modifyQueryUsing: fn ($query) => $query
                        ->withoutGlobalScope(AccessibleBranchScope::class)
                        ->where('is_active', true),
                )
                    ->label('To Branch')
                    ->required()
                    ->rule(function (Get $get) {
                        return function (string $attribute, $value, \Closure $fail) use ($get): void {
                            if ($value !== null && (int) $value === (int) $get('from_branch_id')) {
                                $fail('The to branch must be different from the from branch.');
                            }
                        };
                    }),
                ProductSelect::make(
                    modifyQueryUsing: fn ($query) => $query->where('is_active', true),
                )->label('Product')->required()->live(),
                RelationSelect::make(
                    name: '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 from-branch pick, not the acting
                    // user's ambient context branch — the two routinely differ.
                    // BatchScope is bypassed for the same reason as in the
                    // adjustment resource.
                    // A deactivated lot is never offered to move: parking it
                    // is precisely the instruction not to put it in front of
                    // anyone. Expired/recalled stock can still be transferred
                    // (returns to a central store, quarantine).
                    modifyQueryUsing: fn ($query, Get $get) => $query
                        ->withoutGlobalScope(BranchScope::class)
                        ->where('product_id', $get('product_id'))
                        ->notDeactivated()
                        ->when(filled($get('from_branch_id')), fn ($q) => $q->where('branch_id', (int) $get('from_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 transferring it.');
                        }
                    })
                    // Every product has lots now (a GRN receipt prices each
                    // one), so transfers name the lot being moved — that keeps
                    // Σ lot.available_quantity reconciled with branch_stock at
                    // both ends. When serials are picked instead, the transfer
                    // is a pure serial move (quantity = serial count).
                    ->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('from_branch_id')), fn ($q) => $q->withoutGlobalScope(BranchScope::class)->where('branch_id', (int) $get('from_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('from_branch_id')) ? (int) $get('from_branch_id') : null,
                        ) ?? false)),
                TextInput::make('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'))),
                Textarea::make('note')->maxLength(500)->columnSpanFull(),
            ])->columns(2),
        ]);
    }

    public static function table(Table $table): Table
    {
        return static::standardTableDefaults($table)
            ->recordUrl(static::standardRecordUrl())
            ->emptyStateHeading('No transfers yet')
            ->emptyStateDescription('Stock moved between branches will appear here.')
            // Rows are visible when EITHER endpoint is accessible (see
            // DualBranchScoped), so the label loads must see both endpoints —
            // an inbound row's source branch and an outbound row's
            // destination routinely sit outside the acting user's own list.
            // The rows themselves stay visibility-scoped; this only keeps
            // the two name columns from rendering blank.
            ->modifyQueryUsing(fn (Builder $query): Builder => $query->with([
                'fromBranch' => fn ($q) => $q->withoutGlobalScope(AccessibleBranchScope::class),
                'toBranch' => fn ($q) => $q->withoutGlobalScope(AccessibleBranchScope::class),
            ]))
            ->columns([
                TextColumn::make('fromBranch.name')->label('From'),
                TextColumn::make('toBranch.name')->label('To'),
                TextColumn::make('product.name')->searchable(),
                TextColumn::make('quantity')
                    ->formatStateUsing(fn ($state, StockTransfer $record): string => Quantity::format($state, $record->unit)),
                TextColumn::make('reason.name'),
                TextColumn::make('created_at')->dateTime()->sortable(),
            ])
            ->defaultSort('created_at', 'desc')
            ->filters([
                SelectFilter::make('from_branch_id')->label('From Branch')->relationship('fromBranch', 'name'),
            ])
            ->recordActions([
                DeleteAction::make()
                    ->before(function (StockTransfer $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' => ListStockTransfers::route('/'),
            'create' => CreateStockTransfer::route('/create'),
        ];
    }

    public static function assertSourceBranchAccessible(int $branchId): void
    {
        abort_unless(
            app(CurrentContext::class)->canAccessBranch($branchId),
            403,
            'You do not have access to the source branch.',
        );
    }

    /**
     * Ported verbatim from StockTransferController::store().
     *
     * @param  array<string, mixed>  $data
     */
    public static function apply(array $data): StockTransfer
    {
        $serialIds = SerialInput::ids($data['serial_ids'] ?? []);

        $quantity = $serialIds !== [] ? (float) count($serialIds) : (float) $data['quantity'];

        return DB::transaction(function () use ($data, $serialIds, $quantity) {
            $stockBatchId = isset($data['stock_batch_id']) ? (int) $data['stock_batch_id'] : null;

            $transfer = StockTransfer::create([
                'from_branch_id' => (int) $data['from_branch_id'],
                'to_branch_id' => (int) $data['to_branch_id'],
                'product_id' => (int) $data['product_id'],
                'unit_id' => isset($data['unit_id']) ? (int) $data['unit_id'] : null,
                'reason_id' => $data['reason_id'],
                'quantity' => $quantity,
                'stock_batch_id' => $stockBatchId,
                'serial_ids' => $serialIds ?: null,
                'note' => $data['note'] ?? null,
            ]);

            $transferNumber = 'TRF-'.date('Y').'-'.str_pad((string) $transfer->id, 5, '0', STR_PAD_LEFT);

            app(StockMovementService::class)->record(
                branchId: (int) $data['from_branch_id'],
                productId: (int) $data['product_id'],
                movementTypeCode: MovementType::TRANSFER_OUT,
                quantityChange: -$quantity,
                referenceType: $transfer,
                referenceNumber: $transferNumber,
                note: $data['note'] ?? null,
                stockBatchId: $stockBatchId,
            );

            app(StockMovementService::class)->record(
                branchId: (int) $data['to_branch_id'],
                productId: (int) $data['product_id'],
                movementTypeCode: MovementType::TRANSFER_IN,
                quantityChange: $quantity,
                referenceType: $transfer,
                referenceNumber: $transferNumber,
                note: $data['note'] ?? null,
                stockBatchId: $stockBatchId,
            );

            if ($stockBatchId) {
                app(BatchService::class)->transferPortion(StockBatch::findOrFail($stockBatchId), $quantity, (int) $data['to_branch_id']);
            }

            if ($serialIds) {
                app(SerialService::class)->transfer($serialIds, (int) $data['to_branch_id']);
            }

            return $transfer;
        });
    }

    /**
     * Ported verbatim from StockTransferController::destroy().
     */
    public static function reverse(StockTransfer $transfer): void
    {
        DB::transaction(function () use ($transfer): void {
            $transferNumber = 'TRF-'.date('Y', strtotime((string) $transfer->created_at)).'-'.str_pad((string) $transfer->id, 5, '0', STR_PAD_LEFT);
            $qty = (float) $transfer->quantity;

            app(StockMovementService::class)->record(
                branchId: (int) $transfer->from_branch_id,
                productId: (int) $transfer->product_id,
                movementTypeCode: MovementType::TRANSFER_REVERSAL,
                quantityChange: $qty,
                referenceType: $transfer,
                referenceNumber: $transferNumber,
                note: 'Transfer deletion reversal',
                stockBatchId: $transfer->stock_batch_id,
            );

            app(StockMovementService::class)->record(
                branchId: (int) $transfer->to_branch_id,
                productId: (int) $transfer->product_id,
                movementTypeCode: MovementType::TRANSFER_REVERSAL,
                quantityChange: -$qty,
                referenceType: $transfer,
                referenceNumber: $transferNumber,
                note: 'Transfer deletion reversal',
                stockBatchId: $transfer->stock_batch_id,
            );

            if ($transfer->stock_batch_id) {
                // Same BranchScope bypass as the source-side picker: the
                // acting user is usually scoped to only one of the two
                // branches, and BOTH lots can sit outside it — the source on
                // the from-branch, its counterpart on the to-branch.
                // findCounterpart() bypasses the scope on its own side.
                $sourceBatch = StockBatch::query()
                    ->withoutGlobalScope(BranchScope::class)
                    ->find($transfer->stock_batch_id);

                $destinationBatch = $sourceBatch
                    ? app(BatchService::class)->findCounterpart($sourceBatch, (int) $transfer->to_branch_id)
                    : null;

                if ($destinationBatch) {
                    app(BatchService::class)->transferPortion($destinationBatch, $qty, (int) $transfer->from_branch_id);
                }
            }

            if ($transfer->serial_ids) {
                app(SerialService::class)->transfer($transfer->serial_ids, (int) $transfer->from_branch_id);
            }

            AuditLog::record('stock_transfer.deleted', $transfer, [
                'from_branch_id' => $transfer->from_branch_id,
                'to_branch_id' => $transfer->to_branch_id,
                'product_id' => $transfer->product_id,
                'quantity_reversed' => (string) $qty,
            ]);
        });
    }
}
