<?php

declare(strict_types=1);

namespace App\Business\Purchase;

use App\Business\Product\DberpDocumentLineUnitService;
use App\Business\Product\ProductSkuOperationalGuard;
use App\Business\Purchase\Enums\PurchaseOrderStatus;
use App\Business\Settlement\PayableService;
use App\Models\DberpProductSku;
use App\Models\DberpPurchaseInquiryQuote;
use App\Models\DberpPurchaseOrder;
use App\Models\DberpPurchaseOrderItem;
use App\Models\DberpPurchaseRequestItem;
use App\Services\DberpAuditService;
use App\Support\DberpAuditTrailGuard;
use App\Support\DberpDocumentLineBulkProductAddFormFields;
use App\Support\DberpProductSkuSelectOptions;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

final class PurchaseOrderService
{
    public function __construct(
        private readonly DberpAuditService $auditService,
        private readonly PayableService $payableService,
        private readonly PurchaseOrderPriceMatchService $purchaseOrderPriceMatchService,
        private readonly PurchasePriceReferenceService $purchasePriceReferenceService,
        private readonly PurchaseOrderExecutionService $purchaseOrderExecutionService,
        private readonly PurchaseSourceDocumentService $sourceDocumentService,
    ) {}

    /**
     * @param  array<string, mixed>  $data
     */
    public function createItem(DberpPurchaseOrder $order, array $data): DberpPurchaseOrderItem
    {
        return DB::transaction(function () use ($order, $data): DberpPurchaseOrderItem {
            $lockedOrder = DberpPurchaseOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();

            $payload = $this->purchaseOrderPriceMatchService->prepareItemPayload($lockedOrder, $data);
            $this->assertSourceBackedItemPayload($lockedOrder, $payload);

            return $lockedOrder->items()->create($payload);
        });
    }

    /**
     * 批量手工添加明细：不关联采购申请/询价，不占用申请数量、不写转单台账。
     *
     * @param  list<int>  $productSkuIds
     * @return list<DberpPurchaseOrderItem>
     */
    public function createManualItemsFromSkus(
        DberpPurchaseOrder $order,
        array $productSkuIds,
        string $quantity,
        ?string $remark = null,
        bool $skipExistingSkuLines = false,
    ): array {
        if ($productSkuIds === []) {
            throw ValidationException::withMessages([
                DberpDocumentLineBulkProductAddFormFields::FIELD_PRODUCT_SKU_IDS => (string) __('dberp.errors.bulk_add_product_skus_required'),
            ]);
        }

        $formattedQuantity = number_format((float) $quantity, 4, '.', '');
        if ((float) $formattedQuantity <= 0) {
            throw ValidationException::withMessages([
                DberpDocumentLineBulkProductAddFormFields::FIELD_DEFAULT_QUANTITY => (string) __('dberp.errors.bulk_add_quantity_must_be_positive'),
            ]);
        }

        return DB::transaction(function () use ($order, $productSkuIds, $formattedQuantity, $remark, $skipExistingSkuLines): array {
            $lockedOrder = DberpPurchaseOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();

            if ($lockedOrder->status !== PurchaseOrderStatus::Draft->value) {
                throw ValidationException::withMessages([
                    'status' => __('dberp.errors.purchase_order_not_draft'),
                ]);
            }

            $existingSkuIds = $skipExistingSkuLines
                ? $lockedOrder->items()
                    ->whereNotNull('product_sku_id')
                    ->pluck('product_sku_id')
                    ->map(fn (mixed $id): int => (int) $id)
                    ->all()
                : [];

            $nextSort = (int) ($lockedOrder->items()->max('sort_order') ?? 0);
            $created = [];

            foreach ($productSkuIds as $skuId) {
                if ($skipExistingSkuLines && in_array($skuId, $existingSkuIds, true)) {
                    continue;
                }

                $sku = DberpProductSkuSelectOptions::queryPurchasableSkus()
                    ->whereKey($skuId)
                    ->first();

                if (! $sku instanceof DberpProductSku) {
                    throw ValidationException::withMessages([
                        DberpDocumentLineBulkProductAddFormFields::FIELD_PRODUCT_SKU_IDS => (string) __('dberp.errors.bulk_add_product_sku_not_purchasable'),
                    ]);
                }

                $sku->loadMissing(['product.unit']);
                $product = $sku->product;
                if ($product === null) {
                    continue;
                }

                $nextSort++;

                $basePayload = [
                    'product_id' => (int) $product->getKey(),
                    'product_sku_id' => (int) $sku->getKey(),
                    'unit_id' => DberpDocumentLineUnitService::defaultUnitIdForProduct($product),
                    'quantity' => $formattedQuantity,
                    'remark' => $remark,
                    'sort_order' => $nextSort,
                    'purchase_request_item_id' => null,
                    'source_inquiry_quote_id' => null,
                ];

                $payload = $this->purchaseOrderPriceMatchService->prepareItemPayload($lockedOrder, $basePayload);
                $payload['purchase_request_item_id'] = null;
                $payload['source_inquiry_quote_id'] = null;

                $created[] = $lockedOrder->items()->create($payload);
                $existingSkuIds[] = $skuId;
            }

            if ($created !== []) {
                $this->refreshTotals($lockedOrder->fresh() ?? $lockedOrder);
            }

            return $created;
        });
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function updateItem(DberpPurchaseOrderItem $item, array $data): DberpPurchaseOrderItem
    {
        return DB::transaction(function () use ($item, $data): DberpPurchaseOrderItem {
            $lockedItem = DberpPurchaseOrderItem::query()->whereKey($item->getKey())->lockForUpdate()->firstOrFail();
            $lockedOrder = DberpPurchaseOrder::query()->whereKey($lockedItem->order_id)->lockForUpdate()->firstOrFail();

            if ($lockedOrder->status !== PurchaseOrderStatus::Draft->value) {
                throw ValidationException::withMessages([
                    'status' => __('dberp.errors.purchase_order_not_draft'),
                ]);
            }

            if ($this->itemHasLockedSourceFields($lockedItem)) {
                $this->assertSourceBackedItemNotMutatingLockedFields($lockedItem, $data);
            }

            $payload = $this->purchaseOrderPriceMatchService->prepareItemPayload($lockedOrder, array_merge(
                $lockedItem->only($lockedItem->getFillable()),
                $data,
            ));

            $this->assertSourceBackedItemPayload($lockedOrder, $payload, $lockedItem);

            $lockedItem->fill($payload)->save();

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

    /**
     * @param  array<string, mixed>  $payload
     */
    private function assertSourceBackedItemPayload(
        DberpPurchaseOrder $order,
        array $payload,
        ?DberpPurchaseOrderItem $ignoreItem = null,
    ): void {
        $requestItemId = isset($payload['purchase_request_item_id']) ? (int) $payload['purchase_request_item_id'] : null;
        $quoteId = isset($payload['source_inquiry_quote_id']) ? (int) $payload['source_inquiry_quote_id'] : null;
        $quantity = (float) ($payload['quantity'] ?? 0);

        if ($requestItemId === null && $quoteId === null) {
            return;
        }

        if ($requestItemId !== null && $quantity > 0) {
            $requestItem = DberpPurchaseRequestItem::query()->with('request')->findOrFail($requestItemId);
            $this->sourceDocumentService->assertPurchaseRequestItemConvertible($requestItem, $quantity);
        }

        if ($quoteId !== null) {
            $quote = DberpPurchaseInquiryQuote::query()->findOrFail($quoteId);

            if ((int) $quote->supplier_id !== (int) $order->supplier_id) {
                throw ValidationException::withMessages([
                    'source_inquiry_quote_id' => __('dberp.errors.purchase_order_source_supplier_mismatch'),
                ]);
            }

            $this->sourceDocumentService->assertInquiryQuoteConvertible($quote, $quantity > 0 ? $quantity : (float) ($ignoreItem?->quantity ?? 0));

            if ($requestItemId !== null && (int) $quote->purchase_request_item_id !== $requestItemId) {
                throw ValidationException::withMessages([
                    'source_inquiry_quote_id' => __('dberp.errors.purchase_order_source_inquiry_quote_mismatch'),
                ]);
            }
        }
    }

    /**
     * @param  array<string, mixed>  $data
     */
    private function assertSourceBackedItemNotMutatingLockedFields(DberpPurchaseOrderItem $item, array $data): void
    {
        foreach (['purchase_request_item_id', 'source_inquiry_quote_id'] as $field) {
            if (! array_key_exists($field, $data)) {
                continue;
            }

            if ((int) ($data[$field] ?? 0) !== (int) ($item->{$field} ?? 0)) {
                throw ValidationException::withMessages([
                    $field => __('dberp.errors.purchase_order_source_line_locked'),
                ]);
            }
        }

        if (array_key_exists('quantity', $data) && abs((float) $data['quantity'] - (float) $item->quantity) > 0.00005) {
            throw ValidationException::withMessages([
                'quantity' => __('dberp.errors.purchase_order_source_line_locked'),
            ]);
        }
    }

    private function itemHasLockedSourceFields(DberpPurchaseOrderItem $item): bool
    {
        return $item->purchase_request_item_id !== null || $item->source_inquiry_quote_id !== null;
    }

    public function confirm(DberpPurchaseOrder $order): void
    {
        DB::transaction(function () use ($order): void {
            $locked = DberpPurchaseOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();

            if ($locked->status !== PurchaseOrderStatus::Draft->value) {
                throw ValidationException::withMessages([
                    'status' => __('dberp.errors.purchase_order_not_draft'),
                ]);
            }

            $items = $locked->items()->with('productSku')->orderBy('sort_order')->orderBy('id')->get();

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

            foreach ($items as $line) {
                if ($line->productSku === null) {
                    throw ValidationException::withMessages([
                        'items' => __('dberp.errors.purchase_order_item_sku_required'),
                    ]);
                }

                ProductSkuOperationalGuard::assertEnabledForPurchase($line->productSku);
            }

            $this->refreshTotals($locked);

            $before = ['status' => $locked->status, 'confirmed_at' => $locked->confirmed_at];

            $this->auditService->logSemanticSave(
                'dberp.purchase_order.confirmed',
                $locked,
                function () use ($locked): void {
                    $locked->forceFill([
                        'status' => PurchaseOrderStatus::Confirmed->value,
                        'confirmed_at' => now(),
                    ])->save();
                },
                $before,
                ['status' => $locked->status, 'confirmed_at' => $locked->confirmed_at],
            );

            $this->payableService->refreshForPurchaseOrder(
                order: $locked,
                changeType: 'purchase_order_confirmed',
                remark: $locked->remark,
                source: $locked,
                documentNumber: (string) $locked->code,
                businessTime: $locked->business_time,
            );

            foreach ($items as $item) {
                $this->purchasePriceReferenceService->refreshForProduct((int) $item->product_id, (int) $locked->supplier_id);
            }

            $this->purchaseOrderExecutionService->syncOrder($locked);
        });
    }

    public function close(DberpPurchaseOrder $order): void
    {
        DB::transaction(function () use ($order): void {
            $locked = DberpPurchaseOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();

            if (! in_array($locked->status, [
                PurchaseOrderStatus::Confirmed->value,
                PurchaseOrderStatus::PartiallyReceived->value,
                PurchaseOrderStatus::FullyReceived->value,
            ], true)) {
                throw ValidationException::withMessages([
                    'status' => __('dberp.errors.purchase_order_not_closable'),
                ]);
            }

            $before = ['status' => $locked->status, 'closed_at' => $locked->closed_at];

            $this->auditService->logSemanticSave(
                'dberp.purchase_order.closed',
                $locked,
                function () use ($locked): void {
                    $locked->forceFill([
                        'status' => PurchaseOrderStatus::Closed->value,
                        'closed_at' => now(),
                    ])->save();
                },
                $before,
                ['status' => $locked->status, 'closed_at' => $locked->closed_at],
            );
        });
    }

    public function void(DberpPurchaseOrder $order): void
    {
        DB::transaction(function () use ($order): void {
            $locked = DberpPurchaseOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();

            if (! in_array($locked->status, [PurchaseOrderStatus::Draft->value, PurchaseOrderStatus::Confirmed->value], true)) {
                throw ValidationException::withMessages([
                    'status' => __('dberp.errors.purchase_order_not_voidable'),
                ]);
            }

            if ($locked->receipts()->where('status', 'effective')->exists()) {
                throw ValidationException::withMessages([
                    'status' => __('dberp.errors.purchase_order_has_effective_receipts'),
                ]);
            }

            if ($locked->returns()->where('status', 'effective')->exists()) {
                throw ValidationException::withMessages([
                    'status' => __('dberp.errors.purchase_order_has_effective_returns'),
                ]);
            }

            $before = ['status' => $locked->status, 'voided_at' => $locked->voided_at];

            $this->sourceDocumentService->reverseOccupationForPurchaseOrder($locked->fresh(['items']));

            if ($locked->purchase_request_id !== null) {
                $this->sourceDocumentService->refreshPurchaseRequestProgress((int) $locked->purchase_request_id);
            }

            $this->auditService->logSemanticSave(
                'dberp.purchase_order.voided',
                $locked,
                function () use ($locked): void {
                    $locked->forceFill([
                        'status' => PurchaseOrderStatus::Voided->value,
                        'voided_at' => now(),
                    ])->save();
                },
                $before,
                ['status' => $locked->status, 'voided_at' => $locked->voided_at],
            );

            $this->payableService->refreshForPurchaseOrder(
                order: $locked,
                changeType: 'purchase_order_voided',
                remark: $locked->remark,
                source: $locked,
                documentNumber: (string) $locked->code,
                businessTime: now(),
            );
        });
    }

    public function refreshTotals(DberpPurchaseOrder $order): void
    {
        $totals = $order->items()->selectRaw('COALESCE(SUM(quantity), 0) as total_quantity, COALESCE(SUM(line_amount), 0) as total_amount')->first();

        DberpAuditTrailGuard::withoutAuditTrail(function () use ($order, $totals): void {
            $order->forceFill([
                'total_quantity' => (string) ($totals?->total_quantity ?? '0.0000'),
                'total_amount' => (string) ($totals?->total_amount ?? '0.0000'),
            ])->save();
        });
    }

    public function syncReceiptStatus(DberpPurchaseOrder $order): void
    {
        $this->purchaseOrderExecutionService->syncOrder($order);
    }
}
