<?php

declare(strict_types=1);

namespace App\Business\Purchase;

use App\Business\MasterData\DberpDocumentAddressSnapshotService;
use App\Business\Purchase\Enums\PurchaseOrderSourceMode;
use App\Models\DberpProduct;
use App\Models\DberpProductSku;
use App\Models\DberpPurchaseInquiryItem;
use App\Models\DberpPurchaseInquiryQuote;
use App\Models\DberpPurchaseOrder;
use App\Models\DberpPurchaseRequest;
use App\Models\DberpPurchaseRequestItem;
use App\Services\DberpAuditService;
use App\Support\DberpAuditTrailGuard;
use App\Support\DberpDocumentRemarkPropagation;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

final class PurchaseOrderCreationService
{
    public function __construct(
        private readonly DberpAuditService $auditService,
        private readonly DberpDocumentAddressSnapshotService $documentAddressSnapshotService,
        private readonly PurchaseOrderPriceMatchService $purchaseOrderPriceMatchService,
        private readonly PurchaseOrderService $purchaseOrderService,
        private readonly PurchaseSourceDocumentService $sourceDocumentService,
    ) {}

    /**
     * @param  array<string, mixed>  $data
     */
    public function createFromFormData(array $data): DberpPurchaseOrder
    {
        $mode = PurchaseOrderSourceMode::tryFrom((string) ($data['source_mode'] ?? PurchaseOrderSourceMode::Manual->value))
            ?? PurchaseOrderSourceMode::Manual;

        return match ($mode) {
            PurchaseOrderSourceMode::Manual => $this->createManualOrder($data),
            PurchaseOrderSourceMode::PurchaseRequest => $this->createFromPurchaseRequestLines($data),
            PurchaseOrderSourceMode::InquiryQuote => $this->createFromInquiryQuoteLines($data),
        };
    }

    /**
     * @param  array<string, mixed>  $headerData
     * @param  array<int, float>  $requestItemQuantities
     */
    public function createFromPurchaseRequestItemQuantities(
        array $headerData,
        array $requestItemQuantities,
    ): DberpPurchaseOrder {
        return DB::transaction(function () use ($headerData, $requestItemQuantities): DberpPurchaseOrder {
            $normalized = $this->normalizePositiveQuantitiesById($requestItemQuantities);

            if ($normalized === []) {
                throw ValidationException::withMessages([
                    'lines' => __('dberp.errors.purchase_request_no_convertible_items'),
                ]);
            }

            $requestItems = DberpPurchaseRequestItem::query()
                ->with(['request', 'product', 'warehouse'])
                ->whereIn('id', array_keys($normalized))
                ->get()
                ->keyBy(fn (DberpPurchaseRequestItem $item): int => (int) $item->getKey());

            if ($requestItems->isEmpty()) {
                throw ValidationException::withMessages([
                    'lines' => __('dberp.errors.purchase_request_no_convertible_items'),
                ]);
            }

            $requestIds = $requestItems->pluck('request_id')->unique()->values();

            if ($requestIds->count() > 1) {
                throw ValidationException::withMessages([
                    'purchase_request_id' => __('dberp.errors.purchase_order_source_single_request_required'),
                ]);
            }

            /** @var DberpPurchaseRequest $request */
            $request = DberpPurchaseRequest::query()->whereKey($requestIds->first())->lockForUpdate()->firstOrFail();

            $this->sourceDocumentService->assertPurchaseRequestAllowsDownstreamProcessing($request);

            foreach ($normalized as $itemId => $quantity) {
                $item = $requestItems->get($itemId);

                if (! $item instanceof DberpPurchaseRequestItem) {
                    throw ValidationException::withMessages([
                        'lines' => __('dberp.errors.invalid_purchase_source_document'),
                    ]);
                }

                $this->sourceDocumentService->assertPurchaseRequestItemConvertible($item, $quantity);
            }

            $order = $this->createOrderHeader(array_merge($headerData, [
                'supplier_id' => $headerData['supplier_id'] ?? null,
                'purchase_request_id' => $request->getKey(),
                'purchase_request_code' => $request->code,
                'source_system' => $request->source_system ?: 'dberp',
                'external_reference_id' => $request->external_reference_id,
            ]), mustHaveSupplier: true);

            $sortOrder = 1;

            foreach ($normalized as $itemId => $quantity) {
                /** @var DberpPurchaseRequestItem $requestItem */
                $requestItem = $requestItems->get($itemId);

                $this->purchaseOrderService->createItem($order, [
                    'purchase_request_item_id' => $requestItem->getKey(),
                    'requested_warehouse_id' => $requestItem->warehouse_id,
                    'product_id' => $requestItem->product_id,
                    'product_sku_id' => $this->resolveProductSkuId($requestItem),
                    'quantity' => number_format($quantity, 4, '.', ''),
                    'unit_price' => null,
                    'sort_order' => $requestItem->sort_order ?: $sortOrder,
                    'remark' => $requestItem->remark,
                    'external_product_id' => $requestItem->external_product_id,
                    'external_sku' => $requestItem->external_sku,
                    'unit_id' => $requestItem->unit_id,
                    'unit_name' => $requestItem->unit_name,
                    'base_unit_id' => $requestItem->base_unit_id,
                    'auxiliary_unit_id' => $requestItem->auxiliary_unit_id,
                    'is_auxiliary_unit' => $requestItem->is_auxiliary_unit,
                    'auxiliary_unit_quantity' => $requestItem->auxiliary_unit_quantity,
                    'base_unit_quantity' => $requestItem->base_unit_quantity,
                ]);

                $sortOrder++;
            }

            DberpAuditTrailGuard::withoutAuditTrail(function () use ($request): void {
                $request->forceFill(['last_converted_at' => now()])->save();
            });

            $this->sourceDocumentService->refreshPurchaseRequestProgress((int) $request->getKey());

            $this->auditService->log('dberp.purchase_order.created_from_purchase_request', $order->refresh(), null, [
                'purchase_request_id' => $request->getKey(),
                'purchase_request_code' => $request->code,
                'line_quantities' => $normalized,
            ]);

            return $order->refresh();
        });
    }

    /**
     * @param  array<string, mixed>  $headerData
     * @param  array<int, float>  $quoteQuantities
     */
    public function createFromInquiryQuoteQuantities(
        array $headerData,
        array $quoteQuantities,
    ): DberpPurchaseOrder {
        return DB::transaction(function () use ($headerData, $quoteQuantities): DberpPurchaseOrder {
            $normalized = $this->normalizePositiveQuantitiesById($quoteQuantities);

            if ($normalized === []) {
                throw ValidationException::withMessages([
                    'quotes' => __('dberp.errors.no_selectable_inquiry_quote'),
                ]);
            }

            $quotes = DberpPurchaseInquiryQuote::query()
                ->with(['purchaseInquiryItem.purchaseRequestItem', 'purchaseRequestItem'])
                ->whereIn('id', array_keys($normalized))
                ->get()
                ->keyBy(fn (DberpPurchaseInquiryQuote $quote): int => (int) $quote->getKey());

            if ($quotes->isEmpty()) {
                throw ValidationException::withMessages([
                    'quotes' => __('dberp.errors.no_selectable_inquiry_quote'),
                ]);
            }

            $supplierIds = $quotes->pluck('supplier_id')->unique()->values();

            if ($supplierIds->count() > 1) {
                throw ValidationException::withMessages([
                    'quotes' => __('dberp.errors.purchase_order_source_supplier_mismatch'),
                ]);
            }

            $supplierId = (int) $supplierIds->first();

            if (isset($headerData['supplier_id']) && (int) $headerData['supplier_id'] !== $supplierId) {
                throw ValidationException::withMessages([
                    'supplier_id' => __('dberp.errors.purchase_order_source_supplier_mismatch'),
                ]);
            }

            $requestItemTotals = [];

            foreach ($normalized as $quoteId => $quantity) {
                $quote = $quotes->get($quoteId);

                if (! $quote instanceof DberpPurchaseInquiryQuote) {
                    throw ValidationException::withMessages([
                        'quotes' => __('dberp.errors.invalid_purchase_source_document'),
                    ]);
                }

                $this->sourceDocumentService->assertInquiryQuoteConvertible($quote, $quantity);

                $requestItemId = (int) $quote->purchase_request_item_id;
                $requestItemTotals[$requestItemId] = ($requestItemTotals[$requestItemId] ?? 0) + $quantity;
            }

            foreach ($requestItemTotals as $requestItemId => $totalQuantity) {
                $requestItem = DberpPurchaseRequestItem::query()->with('request')->whereKey($requestItemId)->firstOrFail();
                $this->sourceDocumentService->assertPurchaseRequestItemConvertible($requestItem, $totalQuantity);
            }

            $firstQuote = $quotes->first();
            $request = $firstQuote?->purchaseRequest;

            $order = $this->createOrderHeader(array_merge($headerData, [
                'supplier_id' => $supplierId,
                'purchase_request_id' => $firstQuote?->purchase_request_id,
                'purchase_request_code' => $firstQuote?->purchase_request_code,
                'source_system' => $firstQuote?->source_system ?? 'dberp',
                'external_reference_id' => $request?->external_reference_id,
            ]), mustHaveSupplier: true);

            $sortOrder = 1;

            foreach ($normalized as $quoteId => $quantity) {
                /** @var DberpPurchaseInquiryQuote $quote */
                $quote = $quotes->get($quoteId);
                $requestItem = $quote->purchaseRequestItem;
                $inquiryItem = $quote->purchaseInquiryItem;

                if ($requestItem === null || ! $inquiryItem instanceof DberpPurchaseInquiryItem) {
                    throw ValidationException::withMessages([
                        'quotes' => __('dberp.errors.invalid_purchase_source_document'),
                    ]);
                }

                $warehouseId = $inquiryItem->warehouse_id ?? $requestItem->warehouse_id;

                $this->purchaseOrderService->createItem($order, [
                    'purchase_request_item_id' => $requestItem->getKey(),
                    'source_inquiry_quote_id' => $quote->getKey(),
                    'requested_warehouse_id' => $warehouseId,
                    'product_id' => $quote->product_id,
                    'product_sku_id' => $this->resolveProductSkuId($requestItem, $inquiryItem),
                    'quantity' => number_format($quantity, 4, '.', ''),
                    'unit_price' => $quote->unit_price,
                    'sort_order' => $inquiryItem->sort_order ?: $sortOrder,
                    'remark' => DberpDocumentRemarkPropagation::merge(
                        $requestItem->remark,
                        $quote->quote_note,
                        $quote->remark,
                    ),
                    'external_product_id' => $requestItem->external_product_id,
                    'external_sku' => $requestItem->external_sku ?? $quote->external_sku,
                    'unit_id' => $requestItem->unit_id,
                    'unit_name' => $requestItem->unit_name,
                    'base_unit_id' => $requestItem->base_unit_id,
                    'auxiliary_unit_id' => $requestItem->auxiliary_unit_id,
                    'is_auxiliary_unit' => $requestItem->is_auxiliary_unit,
                    'auxiliary_unit_quantity' => $requestItem->auxiliary_unit_quantity,
                    'base_unit_quantity' => $requestItem->base_unit_quantity,
                ]);

                $sortOrder++;
            }

            if ($order->purchase_request_id !== null) {
                DberpAuditTrailGuard::withoutAuditTrail(function () use ($order): void {
                    DberpPurchaseRequest::query()
                        ->whereKey($order->purchase_request_id)
                        ->update(['last_converted_at' => now()]);
                });

                $this->sourceDocumentService->refreshPurchaseRequestProgress((int) $order->purchase_request_id);
            }

            $this->auditService->log('dberp.purchase_order.created_from_inquiry_quote', $order->refresh(), null, [
                'supplier_id' => $supplierId,
                'quote_ids' => array_keys($normalized),
                'quote_quantities' => $normalized,
            ]);

            return $order->refresh();
        });
    }

    /**
     * @param  array<string, mixed>  $data
     */
    private function createManualOrder(array $data): DberpPurchaseOrder
    {
        return DB::transaction(function () use ($data): DberpPurchaseOrder {
            $order = $this->createOrderHeader($data, mustHaveSupplier: true);

            $this->auditService->log('dberp.purchase_order.created_manual', $order, null, []);

            return $order;
        });
    }

    /**
     * @param  array<string, mixed>  $data
     */
    private function createFromPurchaseRequestLines(array $data): DberpPurchaseOrder
    {
        return $this->createFromPurchaseRequestItemQuantities(
            $this->extractHeaderData($data),
            $this->extractRequestLineQuantities($data),
        );
    }

    /**
     * @param  array<string, mixed>  $data
     */
    private function createFromInquiryQuoteLines(array $data): DberpPurchaseOrder
    {
        return $this->createFromInquiryQuoteQuantities(
            $this->extractHeaderData($data),
            $this->extractQuoteLineQuantities($data),
        );
    }

    /**
     * @param  array<string, mixed>  $data
     * @return array<string, mixed>
     */
    private function extractHeaderData(array $data): array
    {
        return [
            'supplier_id' => $data['supplier_id'] ?? null,
            'handler_employee_id' => $data['handler_employee_id'] ?? null,
            'business_time' => $data['business_time'] ?? now(),
            'remark' => $data['remark'] ?? null,
            'purchase_request_id' => $data['purchase_request_id'] ?? null,
        ];
    }

    /**
     * @param  array<string, mixed>  $data
     * @return array<int, float>
     */
    private function extractRequestLineQuantities(array $data): array
    {
        $quantities = [];

        foreach ((array) ($data['request_lines'] ?? []) as $line) {
            if (! is_array($line)) {
                continue;
            }

            $itemId = (int) ($line['purchase_request_item_id'] ?? 0);
            $quantity = (float) ($line['source_quantity'] ?? 0);

            if ($itemId <= 0 || $quantity <= 0) {
                continue;
            }

            $quantities[$itemId] = $quantity;
        }

        return $quantities;
    }

    /**
     * @param  array<string, mixed>  $data
     * @return array<int, float>
     */
    private function extractQuoteLineQuantities(array $data): array
    {
        $quantities = [];

        foreach ((array) ($data['quote_lines'] ?? []) as $line) {
            if (! is_array($line)) {
                continue;
            }

            $quoteId = (int) ($line['purchase_inquiry_quote_id'] ?? 0);
            $quantity = (float) ($line['source_quantity'] ?? 0);

            if ($quoteId <= 0 || $quantity <= 0) {
                continue;
            }

            $quantities[$quoteId] = $quantity;
        }

        return $quantities;
    }

    /**
     * @param  array<string, mixed>  $data
     */
    private function createOrderHeader(array $data, bool $mustHaveSupplier = false): DberpPurchaseOrder
    {
        $supplierId = (int) ($data['supplier_id'] ?? 0);

        if ($mustHaveSupplier && $supplierId <= 0) {
            throw ValidationException::withMessages([
                'supplier_id' => __('dberp.errors.purchase_order_supplier_required'),
            ]);
        }

        $payload = $this->documentAddressSnapshotService->fillSupplierSnapshot([
            'supplier_id' => $supplierId > 0 ? $supplierId : null,
            'handler_employee_id' => ($data['handler_employee_id'] ?? null) ? (int) $data['handler_employee_id'] : null,
            'purchase_request_id' => ($data['purchase_request_id'] ?? null) ? (int) $data['purchase_request_id'] : null,
            'purchase_request_code' => $data['purchase_request_code'] ?? null,
            'source_system' => $data['source_system'] ?? 'dberp',
            'external_reference_id' => $data['external_reference_id'] ?? null,
            'business_time' => $data['business_time'] ?? now(),
            'remark' => $data['remark'] ?? null,
        ]);

        return DberpPurchaseOrder::query()->create($payload);
    }

    /**
     * @param  array<int, string|int|float>  $quantities
     * @return array<int, float>
     */
    private function normalizePositiveQuantitiesById(array $quantities): array
    {
        $normalized = [];

        foreach ($quantities as $id => $quantity) {
            $id = (int) $id;
            $quantity = (float) $quantity;

            if ($id <= 0 || $quantity <= 0) {
                continue;
            }

            $normalized[$id] = $quantity;
        }

        return $normalized;
    }

    private function resolveProductSkuId(DberpPurchaseRequestItem $requestItem, ?DberpPurchaseInquiryItem $inquiryItem = null): ?int
    {
        if ($inquiryItem?->product_sku_id) {
            return (int) $inquiryItem->product_sku_id;
        }

        if ($requestItem->product_sku_id) {
            return (int) $requestItem->product_sku_id;
        }

        $productId = (int) $requestItem->product_id;

        if ($productId <= 0) {
            return null;
        }

        $skuId = DberpProductSku::query()
            ->where('product_id', $productId)
            ->orderByDesc('is_default')
            ->orderBy('sort_order')
            ->orderBy('id')
            ->value('id');

        if ($skuId !== null) {
            return (int) $skuId;
        }

        $product = DberpProduct::query()->find($productId);

        if (! $product instanceof DberpProduct) {
            return null;
        }

        return (int) DberpProductSku::createDefaultForProduct($product)->getKey();
    }
}
