<?php

declare(strict_types=1);

namespace App\Business\Sales;

use App\Business\MasterData\DberpProductBarcodeLookupResult;
use App\Business\MasterData\DberpProductBarcodeLookupService;
use App\Business\Product\DberpDocumentLineUnitService;
use App\Business\Product\ProductSkuOperationalGuard;
use App\Business\Sales\Enums\CreditCheckResult;
use App\Business\Sales\Enums\SalesOrderStatus;
use App\Business\Settlement\ReceivableService;
use App\Models\DberpCustomer;
use App\Models\DberpProductSku;
use App\Models\DberpSalesOrder;
use App\Models\DberpSalesOrderItem;
use App\Services\DberpAuditService;
use App\Support\DberpAuditTrailGuard;
use App\Support\DberpDocumentLineBarcodeQuickAddFormFields;
use App\Support\DberpDocumentLineBarcodeQuickAddSupport;
use App\Support\DberpDocumentLineBulkProductAddFormFields;
use App\Support\DberpLineProductSkuFormFields;
use App\Support\DberpProductSkuSelectOptions;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

final class SalesOrderService
{
    public function __construct(
        private readonly DberpAuditService $auditService,
        private readonly ReceivableService $receivableService,
        private readonly CustomerCreditControlService $customerCreditControlService,
        private readonly SalesDocumentPricingService $salesDocumentPricingService,
        private readonly DberpProductBarcodeLookupService $barcodeLookupService,
    ) {}

    /**
     * @param  array<string, mixed>  $data
     */
    public function createOrder(array $data): DberpSalesOrder
    {
        return DB::transaction(function () use ($data): DberpSalesOrder {
            /** @var DberpSalesOrder $order */
            $order = DberpSalesOrder::query()->create($this->normalizePayload($data));

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

    /**
     * 批量手工添加销售订单明细：按既有定价服务初始化单价与快照，不影响库存。
     *
     * @param  list<int>  $productSkuIds
     * @return list<DberpSalesOrderItem>
     */
    public function createManualItemsFromSkus(
        DberpSalesOrder $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 = DberpSalesOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();

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

            $lockedOrder->loadMissing('customer');

            $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::querySalableSkus()
                    ->whereKey($skuId)
                    ->first();

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

                $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,
                    'discount_amount' => '0.0000',
                    'remark' => $remark,
                    'sort_order' => $nextSort,
                ];

                $payload = DberpLineProductSkuFormFields::finalizeLineSkuFormData($basePayload);
                $payload = $this->salesDocumentPricingService->prepareSalesOrderItemData($lockedOrder, $payload);

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

            return $created;
        });
    }

    /**
     * 条码快录单条销售订单明细：按定价服务初始化单价，不影响库存。
     *
     * @param  array<string, mixed>  $formData
     * @return string created|increased|skipped
     */
    public function quickAddItemByBarcode(DberpSalesOrder $order, array $formData): string
    {
        $lookup = $this->barcodeLookupService->lookup(
            (string) ($formData[DberpDocumentLineBarcodeQuickAddFormFields::FIELD_BARCODE] ?? ''),
            DberpProductSkuSelectOptions::PRODUCT_SCOPE_SALABLE,
        );
        $quantity = DberpDocumentLineBarcodeQuickAddFormFields::normalizeQuantity(
            $formData[DberpDocumentLineBarcodeQuickAddFormFields::FIELD_QUANTITY] ?? '1',
        );
        $mode = DberpDocumentLineBarcodeQuickAddFormFields::duplicateModeFromData($formData);
        $remark = filled($formData[DberpDocumentLineBarcodeQuickAddFormFields::FIELD_REMARK] ?? null)
            ? (string) $formData[DberpDocumentLineBarcodeQuickAddFormFields::FIELD_REMARK]
            : null;

        return DB::transaction(function () use ($order, $lookup, $quantity, $mode, $remark): string {
            $lockedOrder = DberpSalesOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();

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

            $lockedOrder->loadMissing('customer');

            $existing = DberpDocumentLineBarcodeQuickAddSupport::findMergeableLine(
                $lockedOrder->items(),
                $lookup->productId,
                $lookup->productSkuId,
                $lookup->unitId,
            );

            return DberpDocumentLineBarcodeQuickAddSupport::handleDuplicateMode(
                $mode,
                $existing,
                $quantity,
                'quantity',
                fn () => $this->createSalesOrderLineFromBarcodeLookup($lockedOrder, $lookup, $quantity, $remark),
            );
        });
    }

    private function createSalesOrderLineFromBarcodeLookup(
        DberpSalesOrder $order,
        DberpProductBarcodeLookupResult $lookup,
        string $quantity,
        ?string $remark,
    ): DberpSalesOrderItem {
        $sku = DberpProductSkuSelectOptions::querySalableSkus()
            ->whereKey($lookup->productSkuId)
            ->first();

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

        $nextSort = (int) ($order->items()->max('sort_order') ?? 0) + 1;

        $basePayload = [
            'product_id' => $lookup->productId,
            'product_sku_id' => $lookup->productSkuId,
            'unit_id' => $lookup->unitId,
            'quantity' => $quantity,
            'discount_amount' => '0.0000',
            'remark' => $remark,
            'sort_order' => $nextSort,
        ];

        $payload = DberpLineProductSkuFormFields::finalizeLineSkuFormData($basePayload);
        $payload = $this->salesDocumentPricingService->prepareSalesOrderItemData($order, $payload);

        /** @var DberpSalesOrderItem $line */
        $line = $order->items()->create($payload);

        return $line;
    }

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

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

            $locked->fill($this->normalizePayload($data, $locked));
            $locked->save();

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

    public function confirm(DberpSalesOrder $order, bool $allowOverride = false, ?string $overrideReason = null): void
    {
        $blockedAudit = null;

        try {
            DB::transaction(function () use ($order, $allowOverride, $overrideReason, &$blockedAudit): void {
                $locked = DberpSalesOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();

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

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

                if ($items->isEmpty()) {
                    $this->auditService->log('dberp.sales_order.confirm_blocked_no_items', $locked, null, [
                        'status' => $locked->status,
                    ]);

                    throw ValidationException::withMessages([
                        'items' => __('dberp.errors.sales_order_no_items'),
                    ]);
                }

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

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

                $this->refreshTotals($locked);
                $creditSnapshot = $this->applyCreditDecision($locked, $allowOverride, $overrideReason, $blockedAudit);

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

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

                $this->receivableService->refreshForSalesOrder(
                    order: $locked,
                    changeType: 'sales_order_confirmed',
                    remark: $locked->remark,
                    source: $locked,
                    documentNumber: (string) $locked->code,
                    businessTime: $locked->business_time,
                );
            });
        } catch (ValidationException $exception) {
            if ($blockedAudit !== null) {
                $this->auditService->log('dberp.sales_order.credit_check_blocked', $order, null, $blockedAudit);
            }

            throw $exception;
        }
    }

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

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

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

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

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

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

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

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

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

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

            $this->receivableService->refreshForSalesOrder(
                order: $locked,
                changeType: 'sales_order_voided',
                remark: $locked->remark,
                source: $locked,
                documentNumber: (string) $locked->code,
                businessTime: now(),
            );
        });
    }

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

        $priceSummary = $this->resolvePriceSummary($order);

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

    public function syncOutboundStatus(DberpSalesOrder $order): void
    {
        $locked = DberpSalesOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();

        if (in_array($locked->status, [SalesOrderStatus::Closed->value, SalesOrderStatus::Voided->value], true)) {
            return;
        }

        $items = $locked->items()->get(['quantity', 'shipped_quantity']);
        $totalOrdered = (float) $items->sum(fn ($item) => (float) $item->quantity);
        $totalShipped = (float) $items->sum(fn ($item) => (float) $item->shipped_quantity);

        $status = SalesOrderStatus::Confirmed;

        if ($totalOrdered > 0 && $totalShipped >= $totalOrdered) {
            $status = SalesOrderStatus::FullyShipped;
        } elseif ($totalShipped > 0) {
            $status = SalesOrderStatus::PartiallyShipped;
        }

        if ($locked->status !== $status->value) {
            $before = ['status' => $locked->status];

            $this->auditService->logSemanticSave(
                'dberp.sales_order.outbound_progress_updated',
                $locked,
                function () use ($locked, $status): void {
                    $locked->forceFill(['status' => $status->value])->save();
                },
                $before,
                ['status' => $locked->status],
            );
        }
    }

    /**
     * @param  array<string, mixed>  $data
     * @return array<string, mixed>
     */
    private function normalizePayload(array $data, ?DberpSalesOrder $order = null): array
    {
        $payload = Arr::only($data, [
            'code',
            'customer_id',
            'quote_id',
            'quote_code',
            'handler_employee_id',
            'source_type',
            'source_platform',
            'external_order_id',
            'external_order_no',
            'business_time',
            'discount_rate',
            'payment_term_days',
            'due_date',
            'remark',
        ]);

        $payload['source_type'] = $payload['source_type'] ?? $order?->source_type ?? 'manual';
        $payload['source_platform'] = $payload['source_platform'] ?? $order?->source_platform ?? 'dberp';
        $payload['status'] = $order?->status ?? SalesOrderStatus::Draft->value;
        $payload['discount_rate'] = $payload['discount_rate'] ?? $order?->discount_rate ?? '0.0000';

        $customer = isset($payload['customer_id'])
            ? DberpCustomer::query()->find($payload['customer_id'])
            : ($order?->customer);
        $businessTime = isset($payload['business_time'])
            ? now()->parse((string) $payload['business_time'])
            : ($order?->business_time ?? now());
        $paymentTermDays = $payload['payment_term_days']
            ?? $order?->payment_term_days
            ?? $customer?->default_payment_term_days
            ?? 0;

        $payload['payment_term_days'] = (int) $paymentTermDays;
        $payload['due_date'] = now()->parse($businessTime)->startOfDay()->addDays((int) $paymentTermDays)->toDateString();

        return app('App\\Business\\MasterData\\DberpDocumentAddressSnapshotService')
            ->fillCustomerSnapshotWhenNeeded($payload, $order);
    }

    /**
     * @return array{credit_check_result: string, credit_override_reason: ?string}
     */
    private function applyCreditDecision(DberpSalesOrder $order, bool $allowOverride, ?string $overrideReason, ?array &$blockedAudit = null): array
    {
        $customer = $order->customer()->first();

        if (! $customer instanceof DberpCustomer) {
            throw ValidationException::withMessages([
                'customer_id' => __('dberp.errors.sales_order_customer_required'),
            ]);
        }

        $paymentTermDays = $order->payment_term_days ?? $customer->default_payment_term_days ?? 0;
        $order->forceFill([
            'payment_term_days' => $paymentTermDays,
            'due_date' => $order->business_time?->copy()->startOfDay()->addDays((int) $paymentTermDays)->toDateString(),
        ])->save();

        $result = $this->customerCreditControlService->evaluateCustomer(
            $customer,
            (string) $order->final_amount,
            $order->business_time,
        );

        if ($result['credit_check_result'] === CreditCheckResult::Pass->value) {
            return [
                'credit_check_result' => CreditCheckResult::Pass->value,
                'credit_override_reason' => null,
            ];
        }

        if (! $allowOverride) {
            $blockedAudit = [
                'credit_check_result' => $result['credit_check_result'],
                'credit_check_result_label' => $result['credit_check_result_label'],
                'projected_amount' => $result['projected_amount'],
                'credit_limit_amount' => $result['credit_limit_amount'],
                'overdue_count' => $result['overdue_count'],
                'earliest_due_date' => $result['earliest_due_date'],
            ];

            throw ValidationException::withMessages([
                'credit_check_result' => $this->customerCreditControlService->buildBlockedMessage($result),
            ]);
        }

        if (blank($overrideReason)) {
            throw ValidationException::withMessages([
                'credit_override_reason' => __('dberp.errors.credit_override_reason_required'),
            ]);
        }

        $this->auditService->log('dberp.sales_order.credit_overridden', $order, null, [
            'credit_check_result' => $result['credit_check_result'],
            'credit_override_reason' => $overrideReason,
        ]);

        return [
            'credit_check_result' => CreditCheckResult::ReleasedByOverride->value,
            'credit_override_reason' => (string) $overrideReason,
        ];
    }

    /**
     * @return array{price_source_type: ?string, price_rule_id: ?int}
     */
    private function resolvePriceSummary(DberpSalesOrder $order): array
    {
        $summary = $order->items()
            ->selectRaw('COUNT(DISTINCT price_source_type) as source_count, MIN(price_source_type) as single_source, COUNT(DISTINCT price_rule_id) as rule_count, MIN(price_rule_id) as single_rule')
            ->first();

        return [
            'price_source_type' => ((int) ($summary?->source_count ?? 0) === 1) ? $summary?->single_source : null,
            'price_rule_id' => ((int) ($summary?->rule_count ?? 0) === 1) ? (int) $summary?->single_rule : null,
        ];
    }
}
