<?php

namespace App\Filament\Resources;

use App\Filament\Exports\BarcodeLabelExporter;
use App\Filament\Resources\BarcodeLabelResource\Pages\CreateBarcodeLabel;
use App\Filament\Resources\BarcodeLabelResource\Pages\ListBarcodeLabels;
use App\Filament\Resources\Concerns\HasModulePermissions;
use App\Filament\Resources\Concerns\HasStandardTableFeatures;
use App\Filament\Support\Chip;
use App\Filament\Support\Perm;
use App\Filament\Support\ProductSelect;
use App\Filament\Support\RelationSelect;
use App\Http\Controllers\BarcodeLabelPrintController;
use App\Models\Catalog\Product;
use App\Models\Inventory\BarcodeLabel;
use App\Models\Inventory\ProductSerial;
use App\Models\Inventory\StockBatch;
use App\Models\Lookups\Status;
use App\Models\Scopes\BranchScope;
use App\Services\AuditLog;
use App\Services\BarcodeLabelService;
use App\Support\Quantity;
use Filament\Actions\Action;
use Filament\Actions\BulkAction;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Component;
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\Columns\ViewColumn;
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\Collection;
use Illuminate\Support\Js;

/**
 * The barcodes the shop prints for itself.
 *
 * A manufacturer's barcode says "this is a 500g bag of rice" and stops there,
 * so scanning one at the counter still leaves the cashier choosing a lot, a
 * serial and a quantity by hand. A label minted here says "this is 500g of
 * rice, out of lot BATCH-000412, and that's one scan" — every one of those
 * decisions is made once, at this form, by someone with the stock in front of
 * them. {@see BarcodeLabelService} owns both ends of that.
 *
 * Creating and printing are separate permissions on purpose: reprinting a torn
 * shelf label is routine, inventing a new one changes what a scan is worth.
 */
class BarcodeLabelResource extends Resource
{
    use HasModulePermissions;
    use HasStandardTableFeatures;

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

    protected static string $permissionModuleKey = 'inventory_barcode_labels';

    protected static string $exporter = BarcodeLabelExporter::class;

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

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

    protected static ?string $navigationLabel = 'Barcode Labels';

    protected static ?string $modelLabel = 'barcode label';

    protected static ?int $navigationSort = 60;

    public static function canPrint(): bool
    {
        return Perm::can(static::$permissionModuleKey, 'print');
    }

    public static function form(Schema $schema): Schema
    {
        return $schema->components([
            Section::make('What the label stands for')
                ->description('Everything picked here is baked into the barcode, so scanning it at the till asks nothing.')
                ->schema([
                    RelationSelect::make(
                        name: 'branch_id',
                        relationship: 'branch',
                        titleAttribute: 'name',
                        modifyQueryUsing: fn ($query) => $query->where('is_active', true),
                    )
                        ->label('Branch')
                        ->required()
                        ->live()
                        ->helperText('The label prints against this branch\'s stock, and only its tills honour it.')
                        ->afterStateUpdated(function (Set $set): void {
                            $set('stock_batch_id', null);
                            $set('product_serial_id', null);
                        }),

                    ProductSelect::make(
                        modifyQueryUsing: fn ($query) => $query->where('is_active', true),
                    )
                        ->label('Product')
                        ->required()
                        ->live()
                        // Changing the product invalidates every downstream pick:
                        // another product's lot and its serial.
                        ->afterStateUpdated(function (Set $set): void {
                            $set('stock_batch_id', null);
                            $set('product_serial_id', null);
                            $set('quantity', 1);
                        }),

                    RelationSelect::standalone(
                        name: 'stock_batch_id',
                        modelClass: StockBatch::class,
                        titleAttribute: 'internal_batch_number',
                        // Scoped to the form's own branch pick, never the acting
                        // user's ambient context branch — the same rule the
                        // damage/adjustment forms follow.
                        modifyQueryUsing: fn ($query, Get $get) => $query
                            ->withoutGlobalScope(BranchScope::class)
                            ->where('product_id', $get('product_id'))
                            ->active()
                            ->where('available_quantity', '>', 0)
                            ->whereNotNull('sale_price')
                            ->when(filled($get('branch_id')), fn ($q) => $q->where('branch_id', (int) $get('branch_id'))),
                    )
                        ->label('Lot')
                        ->helperText('The consignment this label is stuck to. The lot is the price, so a scan sells from this one or refuses.')
                        ->visible(fn (Get $get): bool => static::productIsBatchTracked($get('product_id'))
                            && ! static::productIsSerialTracked($get('product_id')))
                        ->required(fn (Get $get): bool => static::productIsBatchTracked($get('product_id'))
                            && ! static::productIsSerialTracked($get('product_id'))),

                    Select::make('product_serial_id')
                        ->label('Serial number')
                        ->searchable()
                        ->helperText('One physical item, one label — the lot comes with the serial, and the quantity is always 1.')
                        ->options(fn (Get $get) => ProductSerial::query()
                            ->withoutGlobalScope(BranchScope::class)
                            ->where('product_id', $get('product_id'))
                            ->when(filled($get('branch_id')), fn ($q) => $q->where('branch_id', (int) $get('branch_id')))
                            ->where('status_id', Status::idOf(Status::CTX_PRODUCT_SERIAL, Status::SERIAL_AVAILABLE))
                            // A serial can only ever wear one label.
                            ->whereDoesntHave('barcodeLabel')
                            ->orderBy('serial_number')
                            ->pluck('serial_number', 'id'))
                        ->visible(fn (Get $get): bool => static::productIsSerialTracked($get('product_id')))
                        ->required(fn (Get $get): bool => static::productIsSerialTracked($get('product_id'))),
                ])
                ->columns(2),

            Section::make('How much one scan adds')
                ->description('One unit unless you say otherwise — this is where a 250 g label, a 6-pack or a full box gets its own barcode.')
                ->schema([
                    TextInput::make('quantity')
                        ->label('Quantity per scan')
                        ->numeric()
                        ->required()
                        ->default(1)
                        ->minValue(0.0001)
                        ->maxValue(1000000)
                        ->step('any')
                        ->disabled(fn (Get $get): bool => static::productIsSerialTracked($get('product_id')))
                        ->helperText(fn (Get $get): ?string => static::unitHelperText($get('product_id'))),

                    TextInput::make('label_name')
                        ->label('Printed name')
                        ->maxLength(150)
                        ->placeholder(fn (Get $get): ?string => Product::find($get('product_id'))?->name)
                        ->helperText('Optional — overrides the product name on the label itself. The till still shows the product.')
                        ->columnSpanFull(),

                    Textarea::make('note')
                        ->maxLength(500)
                        ->rows(2)
                        ->columnSpanFull(),
                ])
                ->columns(2),
        ]);
    }

    public static function table(Table $table): Table
    {
        return static::standardTableDefaults($table)
            ->emptyStateHeading('No barcode labels yet')
            ->emptyStateDescription('Print your own labels so a scan at the till carries the lot, the serial and the quantity with it.')
            ->columns([
                Chip::documentNumber('code', 'Label code')->searchable()->sortable(),
                ViewColumn::make('barcode')
                    ->label('Barcode')
                    ->view('filament.tables.columns.barcode-label-image'),
                TextColumn::make('product.name')->label('Product')->searchable()->wrap(),
                TextColumn::make('batch.internal_batch_number')
                    ->label('Lot')
                    ->placeholder('Unified pool')
                    ->toggleable(),
                TextColumn::make('serial.serial_number')
                    ->label('Serial')
                    ->placeholder('—')
                    ->toggleable(),
                TextColumn::make('quantity')
                    ->label('Per scan')
                    ->formatStateUsing(fn ($state, BarcodeLabel $record): string => Quantity::format(
                        $state,
                        $record->unit ?? $record->product?->baseUnit,
                    )),
                TextColumn::make('branch.name')->label('Branch')->placeholder('—')->toggleable(),
                TextColumn::make('printed_count')
                    ->label('Printed')
                    ->badge()
                    ->color(fn (int $state): string => $state > 0 ? 'success' : 'gray')
                    ->formatStateUsing(fn (int $state): string => $state === 0 ? 'Never' : $state.'×'),
                TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true),
            ])
            ->defaultSort('created_at', 'desc')
            ->filters([
                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}%")),
                    )),
                Filter::make('never_printed')
                    ->label('Never printed')
                    ->query(fn (Builder $query): Builder => $query->where('printed_count', 0)),
            ])
            ->filtersLayout(FiltersLayout::AboveContentCollapsible)
            ->filtersFormColumns(2)
            ->headerActions(static::standardExportActions())
            ->toolbarActions([
                ...static::standardExportBulkActions(),
                BulkAction::make('printLabels')
                    ->label('Print labels')
                    ->icon(Heroicon::OutlinedPrinter)
                    ->authorize(fn (): bool => static::canPrint())
                    ->schema(static::printOptionsSchema())
                    ->action(function (Collection $records, array $data, $livewire): void {
                        static::openPrintSheet($livewire, $records->pluck('id')->all(), $data);
                    }),
            ])
            ->recordActions([
                Action::make('print')
                    ->label('Print')
                    ->icon(Heroicon::OutlinedPrinter)
                    ->authorize(fn (): bool => static::canPrint())
                    ->schema(static::printOptionsSchema())
                    ->action(function (BarcodeLabel $record, array $data, $livewire): void {
                        static::openPrintSheet($livewire, [$record->id], $data);
                    }),
                DeleteAction::make()
                    ->after(fn (BarcodeLabel $record) => AuditLog::record('barcode_label.deleted', $record, [
                        'code' => $record->code,
                        'product' => $record->product?->name,
                    ])),
            ]);
    }

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

    /**
     * The paper decisions, asked once per print run rather than stored on the
     * label: the same label is printed on a roll today and on an A4 sheet
     * tomorrow.
     *
     * @return array<int, Component>
     */
    protected static function printOptionsSchema(): array
    {
        return [
            Select::make('size')
                ->label('Label size')
                ->options(BarcodeLabelPrintController::sizeOptions())
                ->default(BarcodeLabelPrintController::DEFAULT_SIZE)
                ->required(),
            TextInput::make('copies')
                ->label('Copies of each')
                ->numeric()
                ->default(1)
                ->minValue(1)
                ->maxValue(BarcodeLabelPrintController::MAX_COPIES)
                ->required(),
        ];
    }

    /**
     * @param  array<int, int>  $ids
     * @param  array<string, mixed>  $data
     */
    protected static function openPrintSheet($livewire, array $ids, array $data): void
    {
        if ($ids === []) {
            return;
        }

        $url = route('barcode-labels.print', [
            'ids' => implode(',', $ids),
            'size' => $data['size'] ?? BarcodeLabelPrintController::DEFAULT_SIZE,
            'copies' => (int) ($data['copies'] ?? 1),
        ]);

        // A print sheet is a bare document with its own @page size — it belongs
        // in its own tab, not in place of the listing the operator is working
        // through.
        $livewire->js('window.open('.Js::from($url).", '_blank')");
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public static function apply(array $data): BarcodeLabel
    {
        return app(BarcodeLabelService::class)->create([
            'product_id' => (int) $data['product_id'],
            'branch_id' => isset($data['branch_id']) ? (int) $data['branch_id'] : null,
            'stock_batch_id' => isset($data['stock_batch_id']) ? (int) $data['stock_batch_id'] : null,
            'product_serial_id' => isset($data['product_serial_id']) ? (int) $data['product_serial_id'] : null,
            'quantity' => isset($data['quantity']) ? (float) $data['quantity'] : 1.0,
            'unit_id' => isset($data['unit_id']) ? (int) $data['unit_id'] : null,
            'label_name' => $data['label_name'] ?? null,
            'note' => $data['note'] ?? null,
        ]);
    }

    protected static function productIsBatchTracked(mixed $productId): bool
    {
        return filled($productId) && (bool) Product::whereKey($productId)->value('is_batch_tracked');
    }

    protected static function productIsSerialTracked(mixed $productId): bool
    {
        return filled($productId) && (bool) Product::whereKey($productId)->value('is_serial_tracked');
    }

    /**
     * The unit is never picked here — it's whatever is configured on the
     * product ({@see \App\Models\Concerns\HasDefaultUnitFromProduct}, the
     * same rule every other stock document follows). This just tells the
     * operator what "quantity per scan" is counted in.
     */
    protected static function unitHelperText(mixed $productId): ?string
    {
        $unit = filled($productId) ? Product::find($productId)?->baseUnit : null;

        return $unit ? "Counted in the product's unit: {$unit->code}." : null;
    }
}
