<?php

namespace App\Http\Controllers;

use App\Http\Requests\ExchangeSalePacksRequest;
use App\Http\Requests\RecordSalePaymentRequest;
use App\Http\Requests\ReturnSalePacksRequest;
use App\Http\Requests\StoreSaleRequest;
use App\Http\Requests\UpdateSaleRequest;
use App\Models\Customer;
use App\Models\CustomerOrderItem;
use App\Models\FactoryProduct;
use App\Models\Sale;
use App\Models\SalePack;
use App\Support\CashSaleCustomer;
use App\Support\CustomerOrderFulfillment;
use App\Support\CustomerPaymentType;
use App\Support\NumberFormat;
use App\Support\ProductionQueueSummary;
use App\Support\PurchaseCurrency;
use App\Support\SaleFulfillment;
use App\Support\SalePaymentLedger;
use App\Support\SaleType;
use App\Support\SupplierPaymentMethod;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;

class SaleController extends Controller
{
    private const PER_PAGE_OPTIONS = [5, 10, 25, 50];

    public function index(Request $request): Response
    {
        $perPage = $request->integer('per_page', 10);
        if (! in_array($perPage, self::PER_PAGE_OPTIONS, true)) {
            $perPage = 10;
        }

        $sales = Sale::query()
            ->with([
                'customer:id,full_name',
                'order:id,order_number',
                'returnablePacks' => fn ($query) => $query
                    ->with('product.unit:id,name')
                    ->orderBy('id'),
            ])
            ->withCount([
                'packs',
                'returnablePacks',
            ])
            ->where('type', SaleType::SALE)
            ->orderByDesc('sold_at')
            ->orderByDesc('id')
            ->paginate($perPage)
            ->withQueryString()
            ->through(function (Sale $sale): array {
                $row = SaleFulfillment::serializeSaleSummary($sale);
                $returnablePackCount = (int) ($sale->returnable_packs_count ?? 0);

                if ($sale->type !== SaleType::SALE || $returnablePackCount === 0) {
                    return [
                        ...$row,
                        'returnable_pack_count' => 0,
                    ];
                }

                return [
                    ...$row,
                    'returnable_pack_count' => $returnablePackCount,
                ];
            });

        return Inertia::render('sales/index', [
            'sales' => $sales,
            'filters' => [
                'per_page' => $perPage,
            ],
            'currencies' => PurchaseCurrency::values(),
            'paymentMethods' => SupplierPaymentMethod::values(),
        ]);
    }

    public function ready(): Response
    {
        return Inertia::render('sales/ready', [
            'queue' => ProductionQueueSummary::readyForBillingQueue(),
        ]);
    }

    public function create(Request $request): Response
    {
        $itemIds = collect($request->input('item_ids', []))
            ->when($request->filled('item_id'), fn ($ids) => $ids->push($request->integer('item_id')))
            ->map(fn ($id): int => (int) $id)
            ->filter(fn (int $id): bool => $id > 0)
            ->unique()
            ->values()
            ->all();

        $prefill = $this->prefillFromOrderItems($itemIds);
        $defaultCashSale = $request->boolean('cash') && $prefill === null;
        $cashCustomerId = $defaultCashSale
            ? CashSaleCustomer::ensure()->id
            : CashSaleCustomer::id();

        $customers = Customer::query()
            ->where('is_active', true)
            ->tap(fn ($query) => CashSaleCustomer::applyRegisteredScope($query))
            ->orderBy('full_name')
            ->get(['id', 'full_name', 'national_card_number'])
            ->map(fn (Customer $customer): array => [
                'id' => $customer->id,
                'full_name' => $customer->full_name,
                'is_cash_walk_in' => false,
            ])
            ->values()
            ->all();

        if ($cashCustomerId !== null) {
            $walkIn = Customer::query()->find($cashCustomerId);

            if ($walkIn !== null) {
                array_unshift($customers, [
                    'id' => $walkIn->id,
                    'full_name' => $walkIn->full_name,
                    'is_cash_walk_in' => true,
                ]);
            }
        }

        return Inertia::render('sales/create', [
            'customers' => $customers,
            'cashCustomerId' => $cashCustomerId,
            'defaultCashSale' => $defaultCashSale,
            'products' => FactoryProduct::optionsForForm(),
            'prefill' => $prefill,
            'nextBillNumber' => SaleFulfillment::peekNextSaleNumber(),
        ]);
    }

    public function store(StoreSaleRequest $request): RedirectResponse
    {
        try {
            $validated = $request->validated();

            $sale = SaleFulfillment::createSale(
                (int) $validated['customer_id'],
                isset($validated['customer_order_id']) ? (int) $validated['customer_order_id'] : null,
                $validated['sold_at'],
                $validated['items'],
                $validated['notes'] ?? null,
                (float) ($validated['discount'] ?? 0),
            );

            if ($request->boolean('record_cash_payment')) {
                if ($request->user()?->can('sales.payment') !== true) {
                    throw new \InvalidArgumentException(__('You cannot record cash payment.'));
                }

                if (! CashSaleCustomer::isCashCustomer((int) $validated['customer_id'])) {
                    throw new \InvalidArgumentException(__('Cash payment is only allowed for the walk-in cash customer.'));
                }

                SalePaymentLedger::recordPayment($sale->fresh(), [
                    'currency' => PurchaseCurrency::AFN,
                    'amount' => $sale->net_amount,
                    'bill_payment_amount' => $sale->net_amount,
                    'paid_at' => $validated['sold_at'],
                    'payment_method' => SupplierPaymentMethod::CASH,
                    'payment_type' => CustomerPaymentType::SALE,
                ]);
            }
        } catch (\InvalidArgumentException $exception) {
            return redirect()
                ->back()
                ->withInput()
                ->withErrors(['sale' => $exception->getMessage()]);
        }

        $status = $request->boolean('record_cash_payment')
            ? __('Sale recorded and paid in cash.')
            : __('Sale recorded.');

        return redirect()
            ->route('sales.show', $sale)
            ->with('status', $status);
    }

    public function edit(Sale $sale): Response|RedirectResponse
    {
        if ($sale->type !== SaleType::SALE) {
            abort(404);
        }

        $sale->load([
            'customer:id,full_name',
            'order:id,order_number',
            'items.product.unit:id,name',
            'items.packs',
        ]);

        $editableItems = $sale->items
            ->filter(function ($item): bool {
                $activePackCount = $item->packs
                    ->filter(fn (SalePack $pack): bool => ! $pack->is_returned && $pack->original_sale_pack_id === null)
                    ->count();

                return $activePackCount > 0;
            })
            ->values();

        if ($editableItems->isEmpty()) {
            abort(404);
        }

        if (SalePaymentLedger::hasPaymentsForSale($sale)) {
            return redirect()
                ->route('sales.show', $sale)
                ->with('status', __('Sales with payments cannot be edited.'));
        }

        return Inertia::render('sales/edit', [
            'products' => FactoryProduct::optionsForForm(
                $editableItems->pluck('factory_product_id')->all(),
            ),
            'sale' => [
                'id' => $sale->id,
                'sale_number' => $sale->sale_number,
                'customer_id' => $sale->customer_id,
                'customer_name' => CashSaleCustomer::isCashCustomer((int) $sale->customer_id)
                    ? CashSaleCustomer::displayName()
                    : $sale->customer?->full_name,
                'is_cash_walk_in' => CashSaleCustomer::isCashCustomer((int) $sale->customer_id),
                'customer_order_id' => $sale->customer_order_id,
                'order_number' => $sale->order?->order_number,
                'sold_at' => $sale->sold_at?->toDateString(),
                'notes' => $sale->notes,
                'discount' => (string) $sale->discount,
                'total_amount' => (string) $sale->total_amount,
                'net_amount' => (string) $sale->net_amount,
                'items' => $editableItems
                    ->map(fn ($item): array => SaleFulfillment::serializeItem($item))
                    ->all(),
            ],
        ]);
    }

    public function update(UpdateSaleRequest $request, Sale $sale): RedirectResponse
    {
        if ($sale->type !== SaleType::SALE) {
            abort(404);
        }

        try {
            $validated = $request->validated();

            SaleFulfillment::updateSale(
                $sale,
                $validated['sold_at'],
                $validated['notes'] ?? null,
                $validated['items'],
                (float) ($validated['discount'] ?? 0),
            );
        } catch (\InvalidArgumentException $exception) {
            return redirect()
                ->back()
                ->withInput()
                ->withErrors(['sale' => $exception->getMessage()]);
        }

        return redirect()
            ->route('sales.show', $sale)
            ->with('status', __('Sale updated.'));
    }

    public function show(Sale $sale): Response
    {
        $sale->load([
            'customer:id,full_name,contact_number,address',
            'order:id,order_number,ordered_at',
            'relatedSale:id,sale_number,type',
            'items.product.unit:id,name',
            'packs.product.unit:id,name',
        ]);

        $relatedSalesCollection = match (true) {
            $sale->exchange_group_id !== null => Sale::query()
                ->where('exchange_group_id', $sale->exchange_group_id)
                ->whereKeyNot($sale->id)
                ->withCount('packs')
                ->get(),
            $sale->type === SaleType::SALE => Sale::query()
                ->where('related_sale_id', $sale->id)
                ->whereIn('type', [SaleType::RETURN, SaleType::EXCHANGE])
                ->withCount('packs')
                ->get(),
            default => collect(),
        };

        $returnSalesByGroup = Sale::query()
            ->where('type', SaleType::RETURN)
            ->whereIn(
                'exchange_group_id',
                $relatedSalesCollection->pluck('exchange_group_id')->filter()->unique()->values(),
            )
            ->with('packs.product.unit:id,name')
            ->get()
            ->keyBy('exchange_group_id');

        $exchangeTransactions = [];
        $relatedSales = [];

        foreach ($relatedSalesCollection as $related) {
            if ($related->type === SaleType::EXCHANGE) {
                $returnSale = $related->exchange_group_id !== null
                    ? $returnSalesByGroup->get($related->exchange_group_id)
                    : null;
                $transactionLines = SaleFulfillment::serializeExchangeTransaction($returnSale, $related);

                $exchangeTransactions[] = [
                    'id' => $related->id,
                    'sale_number' => $related->sale_number,
                    'return_sale_number' => $returnSale?->sale_number,
                    'sold_at' => $related->sold_at?->toDateTimeString(),
                    'net_amount' => (string) $related->net_amount,
                    'transaction_lines' => $transactionLines,
                    'transaction_summary' => SaleFulfillment::exchangeTransactionSummary($transactionLines),
                ];

                continue;
            }

            if ($related->type === SaleType::RETURN && $related->exchange_group_id !== null) {
                continue;
            }

            $relatedSales[] = SaleFulfillment::serializeSaleSummary($related);
        }

        $displayItems = $sale->items->isNotEmpty()
            ? $sale->items
                ->filter(fn ($item): bool => (float) $item->quantity > 0.001)
                ->map(fn ($item): array => SaleFulfillment::serializeItem($item))
                ->values()
                ->all()
            : SaleFulfillment::displayLinesFromPacks($sale->packs);

        return Inertia::render('sales/show', [
            'sale' => [
                ...SaleFulfillment::serializeSaleSummary($sale),
                'customer_contact' => $sale->customer?->contact_number,
                'customer_address' => $sale->customer?->address,
                'ordered_at' => $sale->order?->ordered_at?->toDateString(),
                'related_sale_number' => $sale->relatedSale?->sale_number,
                'related_sale_type' => $sale->relatedSale?->type,
                'items' => $displayItems,
                'packs' => $sale->packs->map(fn (SalePack $pack): array => SaleFulfillment::serializePack($pack))->values()->all(),
            ],
            'relatedSales' => $relatedSales,
            'exchangeTransactions' => $exchangeTransactions,
            'products' => FactoryProduct::optionsForForm(
                $sale->packs->pluck('factory_product_id')->all(),
            ),
        ]);
    }

    public function returnCreate(Request $request, Sale $sale): Response
    {
        if ($sale->type !== SaleType::SALE) {
            abort(404);
        }

        $sale->load([
            'customer:id,full_name',
            'order:id,order_number',
            'items.product.unit:id,name',
            'packs.product.unit:id,name',
        ]);

        $returnablePacks = $sale->packs
            ->filter(fn (SalePack $pack): bool => ! $pack->is_returned && $pack->original_sale_pack_id === null)
            ->values();

        if ($returnablePacks->isEmpty()) {
            abort(404);
        }

        $lines = SaleFulfillment::returnableLinesFromSale($sale, $returnablePacks);

        if ($lines === []) {
            abort(404);
        }

        $returnableIds = $returnablePacks->pluck('id')->all();

        $preselectedPackIds = collect($request->input('pack_ids', []))
            ->map(fn ($id): int => (int) $id)
            ->filter(fn (int $id): bool => in_array($id, $returnableIds, true))
            ->unique()
            ->values()
            ->all();

        return Inertia::render('sales/return', [
            'sale' => [
                ...SaleFulfillment::serializeSaleSummary($sale),
                'order_number' => $sale->order?->order_number,
            ],
            'lines' => $lines,
            'preselectedPackIds' => $preselectedPackIds,
        ]);
    }

    public function returnPacks(ReturnSalePacksRequest $request, Sale $sale): RedirectResponse
    {
        try {
            $returnSale = SaleFulfillment::returnPacks(
                $sale,
                $request->validated('return_lines'),
                $request->validated('notes'),
            );
        } catch (\InvalidArgumentException $exception) {
            return redirect()
                ->back()
                ->withErrors(['sale' => $exception->getMessage()]);
        }

        return redirect()
            ->route('sales.show', $sale)
            ->with('status', __('Return recorded.'));
    }

    public function exchangeCreate(Sale $sale): Response
    {
        abort_unless(
            auth()->user()?->can('sales.exchange') || auth()->user()?->can('sales.return'),
            403,
        );

        if ($sale->type !== SaleType::SALE) {
            abort(404);
        }

        $sale->load([
            'customer:id,full_name',
            'order:id,order_number',
            'items.product.unit:id,name',
            'packs.product.unit:id,name',
        ]);

        $returnablePacks = $sale->packs
            ->filter(fn (SalePack $pack): bool => ! $pack->is_returned && $pack->original_sale_pack_id === null)
            ->values();

        if ($returnablePacks->isEmpty()) {
            abort(404);
        }

        $lines = SaleFulfillment::returnableLinesFromSale($sale, $returnablePacks);

        if ($lines === []) {
            abort(404);
        }

        return Inertia::render('sales/exchange', [
            'sale' => [
                ...SaleFulfillment::serializeSaleSummary($sale),
                'order_number' => $sale->order?->order_number,
            ],
            'lines' => $lines,
            'products' => FactoryProduct::optionsForForm(),
        ]);
    }

    public function exchangePacks(ExchangeSalePacksRequest $request, Sale $sale): RedirectResponse
    {
        try {
            SaleFulfillment::exchangePacks(
                $sale,
                $request->validated('return_lines'),
                $request->validated('replacement_lines'),
                $request->validated('notes'),
            );
        } catch (\InvalidArgumentException $exception) {
            return redirect()
                ->back()
                ->withErrors(['sale' => $exception->getMessage()]);
        }

        return redirect()
            ->route('sales.show', $sale)
            ->with('status', __('Exchange recorded.'));
    }

    public function recordPayment(
        RecordSalePaymentRequest $request,
        Sale $sale,
    ): RedirectResponse {
        try {
            SalePaymentLedger::recordPayment($sale, [
                ...$request->validated(),
                'payment_type' => CustomerPaymentType::SALE,
            ]);
        } catch (\InvalidArgumentException $exception) {
            return redirect()
                ->back()
                ->withErrors(['payment' => $exception->getMessage()]);
        }

        return redirect()
            ->route('sales.index')
            ->with('status', __('Sale payment recorded.'));
    }

    /**
     * @param  list<int>  $itemIds
     * @return array{
     *     customer_id: int|null,
     *     customer_name: string|null,
     *     customer_order_id: int,
     *     order_number: string|null,
     *     items: list<array{
     *         customer_order_item_id: int,
     *         factory_product_id: int,
     *         product_name: string|null,
     *         unit_name: string|null,
     *         quantity: string,
     *         sold_quantity: string,
     *         remaining_quantity: string
     *     }>
     * }|null
     */
    private function prefillFromOrderItems(array $itemIds): ?array
    {
        if ($itemIds === []) {
            return null;
        }

        $items = CustomerOrderItem::query()
            ->with([
                'product.unit:id,name',
                'order.customer:id,full_name,contact_number',
                'order:id,order_number,customer_id,ordered_at',
            ])
            ->whereIn('id', $itemIds)
            ->get();

        if ($items->count() !== count($itemIds)) {
            abort(404);
        }

        $orderIds = $items->pluck('customer_order_id')->unique();

        if ($orderIds->count() !== 1) {
            abort(404);
        }

        $order = $items->first()?->order;

        return [
            'customer_id' => $order?->customer_id,
            'customer_name' => $order?->customer?->full_name,
            'customer_order_id' => (int) $orderIds->first(),
            'order_number' => $order?->order_number,
            'items' => $items
                ->sortBy('id')
                ->values()
                ->map(fn (CustomerOrderItem $item): array => [
                    'customer_order_item_id' => $item->id,
                    'factory_product_id' => $item->factory_product_id,
                    'product_name' => $item->product?->name,
                    'unit_name' => $item->product?->unit?->name,
                    'quantity' => (string) $item->quantity,
                    'sold_quantity' => (string) $item->sold_quantity,
                    'remaining_quantity' => NumberFormat::quantity(
                        CustomerOrderFulfillment::sellableQuantity($item),
                    ),
                ])
                ->all(),
        ];
    }
}
