<?php

declare(strict_types=1);

namespace App\Business\Purchase;

use App\Business\MasterData\DberpDocumentAddressSnapshotService;
use App\Business\Purchase\Enums\PurchaseInquiryQuoteStatus;
use App\Business\Purchase\Enums\PurchaseRequestStatus;
use App\Models\DberpProduct;
use App\Models\DberpProductSku;
use App\Models\DberpPurchaseInquiryQuote;
use App\Models\DberpPurchaseOrder;
use App\Models\DberpPurchaseRequest;
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 PurchaseRequestConversionService
{
    public function __construct(
        private readonly DberpAuditService $auditService,
        private readonly DberpDocumentAddressSnapshotService $documentAddressSnapshotService,
        private readonly PurchaseRequestService $purchaseRequestService,
        private readonly PurchaseOrderPriceMatchService $purchaseOrderPriceMatchService,
        private readonly PurchaseSourceDocumentService $sourceDocumentService,
    ) {}

    /**
     * @param  array<int, string|int|float>  $itemQuantities
     * @return list<DberpPurchaseOrder>
     */
    public function convertToPurchaseOrders(
        DberpPurchaseRequest $request,
        int $supplierId,
        array $itemQuantities,
        ?int $handlerEmployeeId = null,
        mixed $businessTime = null,
        ?string $remark = null,
    ): array {
        return DB::transaction(function () use ($request, $supplierId, $itemQuantities, $handlerEmployeeId, $businessTime, $remark): array {
            $locked = DberpPurchaseRequest::query()
                ->with(['items.product', 'items.warehouse'])
                ->whereKey($request->getKey())
                ->lockForUpdate()
                ->firstOrFail();

            if (! in_array($locked->status, [
                PurchaseRequestStatus::Pending->value,
                PurchaseRequestStatus::PartiallyConverted->value,
            ], true)) {
                throw ValidationException::withMessages([
                    'status' => __('dberp.errors.purchase_request_not_convertible'),
                ]);
            }

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

            $normalized = $this->normalizeItemQuantities($itemQuantities);

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

            $warehouseGroups = $this->groupConvertibleLinesByWarehouse($locked, $normalized);

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

            $orders = [];
            $allConvertedLines = [];
            $splitByWarehouse = count($warehouseGroups) > 1;
            $remarkBase = DberpDocumentRemarkPropagation::resolveConversionHeaderRemark(
                $remark,
                $locked->remark,
                '由采购申请 '.$locked->code.' 转入。',
            );

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

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

                foreach ($lines as $line) {
                    /** @var DberpPurchaseRequestItem $item */
                    $item = $line['item'];
                    $convertQuantity = (float) $line['quantity'];

                    $recommendedQuote = $this->resolveRecommendedQuoteForConversion($item, $supplierId);

                    $order->items()->create($this->purchaseOrderPriceMatchService->prepareItemPayload($order, [
                        'purchase_request_item_id' => $item->getKey(),
                        'source_inquiry_quote_id' => $recommendedQuote?->getKey(),
                        'requested_warehouse_id' => $requestedWarehouseId,
                        'product_id' => $item->product_id,
                        'product_sku_id' => $this->resolveProductSkuId($item),
                        'quantity' => number_format($convertQuantity, 4, '.', ''),
                        'unit_price' => null,
                        'sort_order' => $item->sort_order,
                        'remark' => $this->resolveConvertedItemRemark($item, $supplierId),
                        'external_product_id' => $item->external_product_id,
                        'external_sku' => $item->external_sku,
                        'unit_id' => $item->unit_id,
                        'unit_name' => $item->unit_name,
                        'base_unit_id' => $item->base_unit_id,
                        'auxiliary_unit_id' => $item->auxiliary_unit_id,
                        'is_auxiliary_unit' => $item->is_auxiliary_unit,
                        'auxiliary_unit_quantity' => $item->auxiliary_unit_quantity,
                        'base_unit_quantity' => $item->base_unit_quantity,
                    ]));

                    $allConvertedLines[] = [
                        'item_id' => $item->getKey(),
                        'product_id' => $item->product_id,
                        'requested_warehouse_id' => $requestedWarehouseId,
                        'quantity' => number_format($convertQuantity, 4, '.', ''),
                        'purchase_order_id' => $order->getKey(),
                    ];
                }

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

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

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

            $this->auditService->log('dberp.purchase_request.converted_to_purchase_order', $locked->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),
                'supplier_id' => $supplierId,
                'split_by_warehouse' => $splitByWarehouse,
                'converted_lines' => $allConvertedLines,
            ]);

            return $orders;
        });
    }

    /**
     * @param  array<int, string|int|float>  $itemQuantities
     */
    public function convertToPurchaseOrder(
        DberpPurchaseRequest $request,
        int $supplierId,
        array $itemQuantities,
        ?int $handlerEmployeeId = null,
        mixed $businessTime = null,
        ?string $remark = null,
    ): DberpPurchaseOrder {
        $orders = $this->convertToPurchaseOrders(
            $request,
            $supplierId,
            $itemQuantities,
            $handlerEmployeeId,
            $businessTime,
            $remark,
        );

        return $orders[0];
    }

    /**
     * @return list<DberpPurchaseOrder>
     */
    public function convertRemainingToPurchaseOrders(
        DberpPurchaseRequest $request,
        int $supplierId,
        ?int $handlerEmployeeId = null,
        mixed $businessTime = null,
        ?string $remark = null,
    ): array {
        $request->loadMissing('items');

        $itemQuantities = $request->items
            ->filter(fn (DberpPurchaseRequestItem $item): bool => (float) $item->remaining_quantity > 0.00005)
            ->mapWithKeys(fn (DberpPurchaseRequestItem $item): array => [$item->getKey() => $item->remaining_quantity])
            ->all();

        return $this->convertToPurchaseOrders($request, $supplierId, $itemQuantities, $handlerEmployeeId, $businessTime, $remark);
    }

    public function convertRemainingToPurchaseOrder(
        DberpPurchaseRequest $request,
        int $supplierId,
        ?int $handlerEmployeeId = null,
        mixed $businessTime = null,
        ?string $remark = null,
    ): DberpPurchaseOrder {
        return $this->convertRemainingToPurchaseOrders($request, $supplierId, $handlerEmployeeId, $businessTime, $remark)[0];
    }

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

        foreach ($itemQuantities as $itemId => $quantity) {
            $itemId = (int) $itemId;
            $quantity = (float) $quantity;

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

            $normalized[$itemId] = $quantity;
        }

        return $normalized;
    }

    /**
     * @param  array<int, float>  $normalized
     * @return array<int, list<array{item: DberpPurchaseRequestItem, quantity: float}>>
     */
    private function groupConvertibleLinesByWarehouse(DberpPurchaseRequest $request, array $normalized): array
    {
        $groups = [];

        foreach ($request->items as $item) {
            $convertQuantity = $normalized[$item->getKey()] ?? 0.0;

            if ($convertQuantity <= 0) {
                continue;
            }

            $this->sourceDocumentService->assertPurchaseRequestItemConvertible($item, $convertQuantity);

            $warehouseKey = $item->warehouse_id !== null && (int) $item->warehouse_id > 0
                ? (int) $item->warehouse_id
                : 0;

            $groups[$warehouseKey] ??= [];
            $groups[$warehouseKey][] = [
                'item' => $item,
                'quantity' => $convertQuantity,
            ];
        }

        ksort($groups);

        return $groups;
    }

    private function resolveRecommendedQuoteForConversion(DberpPurchaseRequestItem $item, int $supplierId): ?DberpPurchaseInquiryQuote
    {
        return DberpPurchaseInquiryQuote::query()
            ->where('purchase_request_item_id', $item->getKey())
            ->where('supplier_id', $supplierId)
            ->where('is_recommended', true)
            ->where('status', PurchaseInquiryQuoteStatus::Active->value)
            ->orderByDesc('quote_date')
            ->orderByDesc('id')
            ->first();
    }

    private function resolveConvertedItemRemark(DberpPurchaseRequestItem $item, int $supplierId): ?string
    {
        $recommendedQuote = $this->resolveRecommendedQuoteForConversion($item, $supplierId);

        return DberpDocumentRemarkPropagation::merge(
            $item->remark,
            $recommendedQuote?->quote_note,
            $recommendedQuote?->remark,
        );
    }

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

        $productId = (int) $item->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();
    }
}
