<?php

declare(strict_types=1);

namespace App\Business\Purchase;

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

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

    /**
     * @param  array<int, string|int|float>  $quoteQuantities
     * @return list<DberpPurchaseOrder>
     */
    public function convertQuotesToPurchaseOrders(
        DberpPurchaseInquiry $inquiry,
        array $quoteQuantities,
        ?int $handlerEmployeeId = null,
        mixed $businessTime = null,
        ?string $remark = null,
    ): array {
        return DB::transaction(function () use ($inquiry, $quoteQuantities, $handlerEmployeeId, $businessTime, $remark): array {
            $lockedInquiry = DberpPurchaseInquiry::query()
                ->with(['purchaseRequest', 'items.purchaseRequestItem'])
                ->whereKey($inquiry->getKey())
                ->lockForUpdate()
                ->firstOrFail();

            $this->assertInquiryAllowsConversion($lockedInquiry);

            $normalized = $this->normalizeQuoteQuantities($quoteQuantities);

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

            $lines = $this->resolveConvertibleQuoteLines($lockedInquiry, $normalized);

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

            $this->assertRequestItemQuantitiesWithinRemaining($lines);

            $orders = [];
            $convertedAuditLines = [];

            $remarkBase = DberpDocumentRemarkPropagation::resolveConversionHeaderRemark(
                $remark,
                $lockedInquiry->remark,
                '由询价单 '.$lockedInquiry->code.' 报价转入。',
            );

            foreach ($this->groupLinesBySupplierAndWarehouse($lines) as $supplierId => $warehouseGroups) {
                $splitByWarehouse = count($warehouseGroups) > 1;

                foreach ($warehouseGroups as $warehouseKey => $supplierLines) {
                    $requestedWarehouseId = $warehouseKey > 0 ? $warehouseKey : null;
                    $orderRemark = $this->buildOrderRemark($remarkBase, $requestedWarehouseId, $splitByWarehouse);

                    $request = $lockedInquiry->purchaseRequest;

                    $order = DberpPurchaseOrder::query()->create(
                        $this->documentAddressSnapshotService->fillSupplierSnapshot([
                            'supplier_id' => $supplierId,
                            'handler_employee_id' => $handlerEmployeeId ?? $lockedInquiry->handler_employee_id,
                            'purchase_request_id' => $lockedInquiry->purchase_request_id,
                            'purchase_request_code' => $lockedInquiry->purchase_request_code,
                            'source_system' => $request?->source_system ?: 'dberp',
                            'external_reference_id' => $request?->external_reference_id,
                            'business_time' => $businessTime ?? now(),
                            'remark' => $orderRemark,
                        ]),
                    );

                    foreach ($supplierLines as $line) {
                        /** @var DberpPurchaseInquiryQuote $quote */
                        $quote = $line['quote'];
                        $convertQuantity = (float) $line['quantity'];
                        $requestItem = $line['request_item'];
                        $inquiryItem = $line['inquiry_item'];

                        $order->items()->create($this->purchaseOrderPriceMatchService->prepareItemPayload($order, [
                            'purchase_request_item_id' => $requestItem->getKey(),
                            'source_inquiry_quote_id' => $quote->getKey(),
                            'requested_warehouse_id' => $requestedWarehouseId,
                            'product_id' => $quote->product_id,
                            'product_sku_id' => $this->resolveProductSkuId($requestItem, $inquiryItem),
                            'quantity' => number_format($convertQuantity, 4, '.', ''),
                            'unit_price' => $quote->unit_price,
                            'sort_order' => $inquiryItem->sort_order,
                            '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,
                        ]));

                        $convertedAuditLines[] = [
                            'quote_id' => $quote->getKey(),
                            'quote_code' => $quote->code,
                            'supplier_id' => $supplierId,
                            'purchase_request_item_id' => $requestItem->getKey(),
                            'quantity' => number_format($convertQuantity, 4, '.', ''),
                            'purchase_order_id' => $order->getKey(),
                        ];
                    }

                    $orders[] = $order->refresh();
                }
            }

            if ($lockedInquiry->purchaseRequest !== null) {
                DberpAuditTrailGuard::withoutAuditTrail(function () use ($lockedInquiry): void {
                    $lockedInquiry->purchaseRequest?->forceFill([
                        'last_converted_at' => now(),
                    ])->save();
                });
            }

            $this->auditService->log('dberp.purchase_inquiry.converted_to_purchase_order', $lockedInquiry->refresh(), null, [
                'purchase_order_ids' => array_map(static fn (DberpPurchaseOrder $order): int => (int) $order->getKey(), $orders),
                'purchase_order_codes' => array_map(static fn (DberpPurchaseOrder $order): string => (string) $order->code, $orders),
                'converted_lines' => $convertedAuditLines,
            ]);

            return $orders;
        });
    }

    public function assertInquiryAllowsConversion(DberpPurchaseInquiry $inquiry): void
    {
        if (! in_array($inquiry->status, [PurchaseInquiryStatus::Open->value], true)) {
            throw ValidationException::withMessages([
                'status' => __('dberp.errors.purchase_inquiry_not_convertible'),
            ]);
        }

        $inquiry->loadMissing('purchaseRequest');

        if ($inquiry->purchaseRequest !== null) {
            $this->sourceDocumentService->assertPurchaseRequestAllowsDownstreamProcessing($inquiry->purchaseRequest);
        }
    }

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

        foreach ($quoteQuantities as $quoteId => $quantity) {
            $quoteId = (int) $quoteId;
            $quantity = (float) $quantity;

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

            $normalized[$quoteId] = $quantity;
        }

        return $normalized;
    }

    /**
     * @param  array<int, float>  $normalized
     * @return list<array{
     *     quote: DberpPurchaseInquiryQuote,
     *     inquiry_item: DberpPurchaseInquiryItem,
     *     request_item: DberpPurchaseRequestItem,
     *     quantity: float
     * }>
     */
    private function resolveConvertibleQuoteLines(DberpPurchaseInquiry $inquiry, array $normalized): array
    {
        $lines = [];

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

        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);

            $requestItem = $quote->purchaseRequestItem;

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

            $inquiryItem = $quote->purchaseInquiryItem;

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

            $lines[] = [
                'quote' => $quote,
                'inquiry_item' => $inquiryItem,
                'request_item' => $requestItem,
                'quantity' => $quantity,
            ];
        }

        return $lines;
    }

    /**
     * @param  list<array{
     *     quote: DberpPurchaseInquiryQuote,
     *     inquiry_item: DberpPurchaseInquiryItem,
     *     request_item: DberpPurchaseRequestItem,
     *     quantity: float
     * }>  $lines
     */
    private function assertRequestItemQuantitiesWithinRemaining(array $lines): void
    {
        $totals = [];

        foreach ($lines as $line) {
            $requestItemId = (int) $line['request_item']->getKey();
            $totals[$requestItemId] = ($totals[$requestItemId] ?? 0) + (float) $line['quantity'];
        }

        foreach ($totals as $requestItemId => $totalQuantity) {
            $requestItem = DberpPurchaseRequestItem::query()
                ->with('request')
                ->whereKey($requestItemId)
                ->firstOrFail();

            $this->sourceDocumentService->assertPurchaseRequestItemConvertible($requestItem, $totalQuantity);
        }
    }

    /**
     * @param  list<array{
     *     quote: DberpPurchaseInquiryQuote,
     *     inquiry_item: DberpPurchaseInquiryItem,
     *     request_item: DberpPurchaseRequestItem,
     *     quantity: float
     * }>  $lines
     * @return array<int, array<int, list<array{
     *     quote: DberpPurchaseInquiryQuote,
     *     inquiry_item: DberpPurchaseInquiryItem,
     *     request_item: DberpPurchaseRequestItem,
     *     quantity: float
     * }>>>
     */
    private function groupLinesBySupplierAndWarehouse(array $lines): array
    {
        $groups = [];

        foreach ($lines as $line) {
            $supplierId = (int) $line['quote']->supplier_id;
            $warehouseKey = $line['inquiry_item']->warehouse_id !== null && (int) $line['inquiry_item']->warehouse_id > 0
                ? (int) $line['inquiry_item']->warehouse_id
                : ($line['request_item']->warehouse_id !== null && (int) $line['request_item']->warehouse_id > 0
                    ? (int) $line['request_item']->warehouse_id
                    : 0);

            $groups[$supplierId] ??= [];
            $groups[$supplierId][$warehouseKey] ??= [];
            $groups[$supplierId][$warehouseKey][] = $line;
        }

        foreach ($groups as $supplierId => $warehouseGroups) {
            ksort($warehouseGroups);
            $groups[$supplierId] = $warehouseGroups;
        }

        ksort($groups);

        return $groups;
    }

    private function buildOrderRemark(string $remarkBase, ?int $requestedWarehouseId, bool $splitByWarehouse): string
    {
        if (! $splitByWarehouse || $requestedWarehouseId === null) {
            return $remarkBase;
        }

        $warehouseName = DberpWarehouse::query()->whereKey($requestedWarehouseId)->value('name');

        if (! is_string($warehouseName) || trim($warehouseName) === '') {
            return $remarkBase;
        }

        return $remarkBase.'（'.__('dberp.messages.purchase_request_convert_target_warehouse', [
            'warehouse' => $warehouseName,
        ]).'）';
    }

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

        if ($requestItem->product_sku_id !== null && (int) $requestItem->product_sku_id > 0) {
            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();
    }
}
