<?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\ReturnResource\Pages\CreateReturn;
use App\Filament\Resources\ReturnResource\Pages\ListReturns;
use App\Filament\Resources\ReturnResource\Pages\ViewReturn;
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\Lookups\ReturnReason;
use App\Models\Lookups\Status;
use App\Models\Returns\ReturnDoc;
use App\Models\Returns\ReturnItem;
use App\Models\Sales\Invoice;
use App\Models\Sales\InvoiceItem;
use App\Models\User;
use App\Services\CurrentContext;
use App\Services\ReturnService;
use App\Support\Quantity;
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\Forms\Components\Toggle;
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\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;

/**
 * Ported from App\Http\Controllers\Isms\ReturnController + ReturnService
 * (docs/migration-plan.md Phase 6).
 *
 * Lifecycle matches the source exactly: a return is created and credited in one
 * step (ReturnService::create() posts the stock movements, issues the credit
 * note or refunds cash, and sets the status to CREDITED) — there is no
 * pending→approve workflow, which is why the module has no approve/cancel verb.
 * There is no edit route in the source either, so no EditRecord page exists.
 * Delete reverses stock and voids the credit note via ReturnService::delete().
 */
class ReturnResource extends Resource
{
    use AssertsBranchAccess;
    use HasModulePermissions;
    use HasStandardTableFeatures;

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

    protected static string $permissionModuleKey = 'isms_returns';

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

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

    protected static ?string $navigationLabel = 'Returns';

    protected static ?int $navigationSort = 40;

    protected static ?string $recordTitleAttribute = 'return_number';

    /** ReturnController's `isms_returns.view_all` gate. */
    public static function canViewAll(): bool
    {
        return Auth::user()?->hasPermissionTo('isms_returns.view_all') ?? false;
    }

    public static function ownsOrViewAll(Model $record): bool
    {
        return static::canViewAll() || $record->created_by === Auth::id();
    }

    public static function canView(Model $record): bool
    {
        return static::hasModulePermission('view') && static::ownsOrViewAll($record);
    }

    public static function canEdit(Model $record): bool
    {
        // No update route exists in the source.
        return false;
    }

    public static function form(Schema $schema): Schema
    {
        return $schema->components([
            Section::make()->schema([
                RelationSelect::make(
                    name: 'customer_id',
                    relationship: 'customer',
                    titleAttribute: 'business_name',
                    modifyQueryUsing: fn ($query) => $query->where('is_active', true),
                )->label('Customer')->live(),
                RelationSelect::make(
                    name: 'branch_id',
                    relationship: 'branch',
                    titleAttribute: 'name',
                    modifyQueryUsing: fn ($query) => $query->where('is_active', true),
                )->label('Branch')->required(),
                Select::make('refund_method')
                    ->options(['cash' => 'Cash', 'credit_note' => 'Credit Note'])
                    ->required()
                    ->default('credit_note'),
                Textarea::make('note')->maxLength(500)->columnSpanFull(),
            ])->columns(3),

            Section::make('Items')->schema([
                Repeater::make('items')
                    ->schema([
                        // Scoped to what this customer actually bought, matching
                        // ReturnController::customerProducts() — a return can only
                        // be raised against something they purchased. Inactive
                        // products stay listed: an old purchase is still
                        // returnable even if the product was later retired.
                        ProductSelect::standalone(
                            modifyQueryUsing: fn (Builder $query, Get $get) => $query
                                ->when(
                                    $get('../../customer_id'),
                                    fn (Builder $q, $customerId) => $q->whereIn(
                                        'id',
                                        InvoiceItem::query()
                                            ->whereNotNull('product_id')
                                            ->whereHas('invoice', fn ($iq) => $iq->where('customer_id', $customerId))
                                            ->distinct()
                                            ->pluck('product_id'),
                                    ),
                                ),
                        )->label('Product')
                            ->required()
                            ->live()
                            ->columnSpanFull()
                            // Product changed → the invoice picker below is
                            // product-scoped, so any stale invoice selection is
                            // dropped. Picking the original invoice then seeds
                            // the unit price and quantity from what the
                            // customer actually paid (see invoice_id below).
                            ->afterStateUpdated(function (Get $get, Set $set, $state): void {
                                $set('invoice_id', null);
                            }),
                        Select::make('invoice_id')
                            ->label('Original invoice')
                            ->options(fn (Get $get): array => static::invoiceOptionsFor(
                                $get('product_id') ? (int) $get('product_id') : null,
                                $get('../../customer_id') ? (int) $get('../../customer_id') : null,
                            ))
                            ->searchable()
                            // Picking the original invoice pre-fills the two
                            // numbers that cause the most typo damage — the
                            // unit price the customer paid and the quantity
                            // still returnable on that line — so neither has
                            // to be typed by hand.
                            ->live()
                            ->afterStateUpdated(function (Get $get, Set $set, $state): void {
                                if (! $state) {
                                    return;
                                }

                                $productId = (int) ($get('product_id') ?? 0);
                                $item = static::invoiceItemFor((int) $state, $productId);
                                if (! $item) {
                                    return;
                                }

                                if ($item->unit_price !== null) {
                                    $set('unit_price', (float) $item->unit_price);
                                }

                                $returnable = static::returnableQuantityFor((int) $state, $productId);
                                if ($returnable !== null && $returnable > 0) {
                                    $set('quantity', $returnable);
                                }
                            }),
                        RelationSelect::standalone(
                            name: 'return_reason_id',
                            modelClass: ReturnReason::class,
                            titleAttribute: 'name',
                        )->label('Reason')->required(),
                        // No unit picker — the product's own base unit applies
                        // automatically (HasDefaultUnitFromProduct).
                        TextInput::make('quantity')
                            ->numeric()->required()->minValue(0.01)->maxValue(1000000)->default(1)
                            ->suffix(fn (Get $get): ?string => Quantity::unitLabel(Product::find($get('product_id'))?->baseUnit))
                            // With an original invoice picked, the input is
                            // capped at what is still returnable on it — the
                            // service rejects over-returns anyway, so this
                            // stops the mistake before it is typed.
                            ->maxValue(fn (Get $get): float => max(0.01, static::returnableQuantityFor(
                                (int) ($get('invoice_id') ?? 0),
                                (int) ($get('product_id') ?? 0),
                            ) ?? 1000000))
                            ->helperText(fn (Get $get): ?string => static::returnableHint(
                                (int) ($get('invoice_id') ?? 0),
                                (int) ($get('product_id') ?? 0),
                            ))
                            ->live(debounce: 500),
                        TextInput::make('unit_price')
                            ->numeric()->required()->minValue(0)->maxValue(99999999.99)->prefix('Rs.')
                            ->hint(fn (Get $get): ?string => static::lineTotalHint($get))
                            ->live(onBlur: true),
                        Select::make('discount_type')
                            ->options(['fixed' => 'Fixed', 'percentage' => 'Percentage'])
                            ->live()
                            ->visible(fn (): bool => Perm::can('isms_returns', 'discount')),
                        TextInput::make('discount_value')
                            ->numeric()->minValue(0)->maxValue(99999999.99)->default(0)
                            ->live(onBlur: true)
                            ->visible(fn (): bool => Perm::can('isms_returns', 'discount')),
                        Toggle::make('is_add_back_to_stock')
                            ->label('Add back to stock')
                            ->default(true)
                            ->helperText('Off for goods too damaged to resell.'),
                    ])
                    // This Section is one of two side by side on the page, so 3
                    // columns cramped a searchable product picker and 6 more
                    // fields into roughly half the viewport.
                    ->columns(2)
                    ->minItems(1)
                    ->addActionLabel('Add item')
                    ->columnSpanFull(),
            ]),
        ]);
    }

    /**
     * Invoices for this customer containing the chosen product — the Filament
     * equivalent of ReturnController::customerInvoices(). Fully-returned lines
     * are dropped so a bill can never be over-returned.
     *
     * @return array<int, string>
     */
    public static function invoiceOptionsFor(?int $productId, ?int $customerId): array
    {
        if (! $productId) {
            return [];
        }

        return Invoice::query()
            ->when($customerId, fn (Builder $q, $id) => $q->where('customer_id', $id))
            ->whereHas('items', fn (Builder $q) => $q->where('product_id', $productId))
            ->with(['items' => fn ($q) => $q->where('product_id', $productId)])
            ->orderByDesc('created_at')
            ->get()
            ->mapWithKeys(function (Invoice $invoice) use ($productId): array {
                $item = $invoice->items->first();
                if (! $item) {
                    return [];
                }

                $remaining = (float) $item->quantity - static::alreadyReturnedQuantity((int) $item->id, (int) $invoice->id, $productId);

                if ($remaining <= 0) {
                    return [];
                }

                return [$invoice->id => $invoice->invoice_number.' — '.rtrim(rtrim(number_format($remaining, 2), '0'), '.').' returnable'];
            })
            ->all();
    }

    /**
     * Ported verbatim from ReturnController::alreadyReturnedQuantity(): matches
     * on the invoice item where we have it, falling back to invoice + product
     * for legacy/walk-in returns raised without a line reference. Uses the
     * fromActiveReturn() scope so a deleted return's lines stop counting.
     */
    public static function alreadyReturnedQuantity(int $invoiceItemId, int $invoiceId, int $productId): float
    {
        $byItem = (float) ReturnItem::fromActiveReturn()
            ->where('invoice_item_id', $invoiceItemId)
            ->sum('quantity');

        $byProduct = (float) ReturnItem::fromActiveReturn()
            ->where('invoice_id', $invoiceId)
            ->where('product_id', $productId)
            ->whereNull('invoice_item_id')
            ->sum('quantity');

        return $byItem + $byProduct;
    }

    /**
     * The invoice line behind a picked (invoice, product) pair — the exact
     * unit price and quantity the customer was sold on the original bill.
     */
    public static function invoiceItemFor(?int $invoiceId, ?int $productId): ?InvoiceItem
    {
        if (! $invoiceId || ! $productId) {
            return null;
        }

        return InvoiceItem::query()
            ->where('invoice_id', $invoiceId)
            ->where('product_id', $productId)
            ->first();
    }

    /**
     * Units of this product on this invoice still returnable (sold minus
     * already returned) — mirrors the cap ReturnService enforces on save.
     */
    public static function returnableQuantityFor(?int $invoiceId, ?int $productId): ?float
    {
        $item = static::invoiceItemFor($invoiceId, $productId);
        if (! $item) {
            return null;
        }

        $remaining = (float) $item->quantity
            - static::alreadyReturnedQuantity((int) $item->id, (int) $item->invoice_id, (int) $item->product_id);

        return max(0, round($remaining, 2));
    }

    /**
     * Live per-line total preview under the unit price, computed exactly like
     * ReturnService::computeDiscount() so what the form shows is what saves.
     */
    public static function lineTotalHint(Get $get): ?string
    {
        $quantity = (float) ($get('quantity') ?? 0);
        $price = (float) ($get('unit_price') ?? 0);
        if ($quantity <= 0 || $price <= 0) {
            return null;
        }

        $lineTotal = $quantity * $price;
        $discountType = $get('discount_type');
        $discountValue = (float) ($get('discount_value') ?? 0);

        $discount = match ($discountType) {
            'percentage' => round($lineTotal * ($discountValue / 100), 2),
            'fixed' => min($lineTotal, $discountValue * $quantity),
            default => 0,
        };

        return 'Line total: Rs. '.number_format(round($lineTotal - $discount, 2), 2);
    }

    protected static function returnableHint(int $invoiceId, int $productId): ?string
    {
        $returnable = static::returnableQuantityFor($invoiceId, $productId);
        if ($returnable === null) {
            return null;
        }

        return rtrim(rtrim(number_format($returnable, 2), '0'), '.').' unit(s) returnable on this invoice';
    }

    public static function table(Table $table): Table
    {
        return static::standardTableDefaults($table)
            ->recordUrl(static::standardRecordUrl())
            ->emptyStateHeading('No returns yet')
            ->emptyStateDescription('Returns logged at the till will appear here.')
            ->modifyQueryUsing(fn (Builder $query): Builder => $query
                ->withTrashed()
                ->withCount('items as total_items')
                ->withCount(['items as stocked_items' => fn ($q) => $q->where('is_add_back_to_stock', true)])
                ->when(! static::canViewAll(), fn (Builder $q) => $q->where('created_by', Auth::id())))
            ->columns([
                Chip::documentNumber('return_number', 'Return #')->searchable()->sortable(),
                TextColumn::make('customer.business_name')->label('Customer')->searchable()->placeholder('Walk-in'),
                TextColumn::make('branch.name')->label('Branch')->placeholder('—'),
                TextColumn::make('status.name')
                    ->label('Status')
                    ->badge()
                    ->color(fn (ReturnDoc $record): string => StatusLookup::colorOf(Status::CTX_RETURN, $record->status?->code ?? '')),
                TextColumn::make('refundMethod.name')->label('Refund')->placeholder('—'),
                TextColumn::make('total')->money(fn () => app(CurrentContext::class)->currencyCode())->sortable(),
                TextColumn::make('refund_amount')->label('Refunded')->money(fn () => app(CurrentContext::class)->currencyCode())->sortable(),
                TextColumn::make('total_items')->label('Items'),
                TextColumn::make('stocked_items')->label('Restocked'),
                TextColumn::make('creator.name')->label('Processed by')->placeholder('—')->toggleable(),
                TextColumn::make('deleted_at')->label('Deleted')->dateTime()->placeholder('—')->toggleable(isToggledHiddenByDefault: true),
                TextColumn::make('created_at')->dateTime()->sortable(),
            ])
            ->defaultSort('created_at', 'desc')
            ->filters([
                SelectFilter::make('status')
                    ->options(fn (): array => StatusLookup::options(Status::CTX_RETURN))
                    ->query(fn (Builder $query, array $data): Builder => $query->when(
                        $data['value'] ?? null,
                        fn (Builder $q, $code) => $q->whereHas('status', fn ($qq) => $qq->where('code', $code)),
                    )),
                // Only offered when the user can see everyone's returns, matching
                // the source's $representatives payload.
                SelectFilter::make('rep')
                    ->label('Processed by')
                    ->visible(fn (): bool => static::canViewAll())
                    ->options(fn (): array => User::query()
                        ->whereIn('id', ReturnDoc::query()->whereNotNull('created_by')->distinct()->pluck('created_by'))
                        ->orderBy('name')
                        ->pluck('name', 'id')
                        ->all())
                    ->query(fn (Builder $query, array $data): Builder => $query->when(
                        $data['value'] ?? null,
                        fn (Builder $q, $rep) => $q->where('created_by', $rep),
                    )),
            ])
            ->recordActions([
                Action::make('view')
                    ->url(fn (ReturnDoc $record): string => ViewReturn::getUrl(['record' => $record]))
                    ->visible(fn (ReturnDoc $record): bool => static::canView($record) && ! $record->trashed()),
                Action::make('delete')
                    ->label('Delete')
                    ->color('danger')
                    ->requiresConfirmation()
                    ->visible(fn (ReturnDoc $record): bool => static::canDelete($record) && ! $record->trashed())
                    ->action(function (ReturnDoc $record): void {
                        try {
                            app(ReturnService::class)->delete($record);
                            Notification::make()->title('Return deleted and stock/credit reversed.')->success()->send();
                        } catch (\Throwable $e) {
                            Notification::make()->title($e->getMessage())->danger()->send();
                        }
                    }),
            ]);
    }

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