<?php

declare(strict_types=1);

namespace App\Business\Inventory;

use App\Business\Inventory\Enums\InventoryBusinessType;
use App\Business\Inventory\Enums\InventoryDocumentStatus;
use App\Business\Inventory\Enums\StocktakeResultType;
use App\Business\Inventory\Exceptions\InsufficientStockException;
use App\Business\Product\InventoryPostingProductSkuResolver;
use App\Models\DberpInventoryBalance;
use App\Models\DberpInventoryTransaction;
use App\Models\DberpProduct;
use App\Models\DberpStocktakeOrder;
use App\Models\DberpStocktakeOrderItem;
use App\Services\DberpAuditService;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

final class StocktakeOrderService
{
    public function __construct(
        private readonly InventoryLedgerService $ledgerService,
        private readonly InventoryRuleService $ruleService,
        private readonly StocktakeDifferenceService $stocktakeDifferenceService,
        private readonly StocktakeSerialNumberDifferenceService $stocktakeSerialNumberDifferenceService,
        private readonly DberpAuditService $auditService,
        private readonly InventoryDocumentLineHintService $lineHintService,
    ) {}

    /**
     * 草稿明细保存：按当前仓库库存余额写入账面数量，并预览差异与盘点结果（确认生效时仍会重算）。
     *
     * @param  array<string, mixed>  $data
     * @return array<string, mixed>
     */
    public function enrichDraftLineFormData(DberpStocktakeOrder $order, array $data): array
    {
        if ($order->status !== InventoryDocumentStatus::Draft->value) {
            return $data;
        }

        $productId = (int) ($data['product_id'] ?? 0);
        if ($productId <= 0 || (int) ($order->warehouse_id ?? 0) <= 0) {
            return $data;
        }

        $product = DberpProduct::query()->find($productId);
        if ($product === null) {
            return $data;
        }

        $lineSkuId = isset($data['product_sku_id']) && $data['product_sku_id'] !== '' && $data['product_sku_id'] !== null
            ? (int) $data['product_sku_id']
            : null;

        try {
            $sku = InventoryPostingProductSkuResolver::resolveSkuForPosting($product, $lineSkuId, 'product_sku_id');
        } catch (ValidationException) {
            return $data;
        }

        $bookScaled = $this->bookScaledIntegerForSku(
            (int) $order->warehouse_id,
            $productId,
            (int) $sku->getKey(),
        );

        $actualBaseString = (string) ($data['base_actual_quantity'] ?? $data['actual_quantity'] ?? '0');
        $actualScaled = $this->toScaledInteger($actualBaseString);
        $differenceScaled = $actualScaled - $bookScaled;

        $data['book_quantity'] = $this->fromScaledInteger($bookScaled);
        $data['difference_quantity'] = $this->fromScaledInteger($differenceScaled);
        $data['result_type'] = $this->resultTypeForDifference($differenceScaled)->value;

        return $data;
    }

    public function syncDraftLineInventorySnapshot(DberpStocktakeOrder $order, DberpStocktakeOrderItem $item): void
    {
        if ($order->status !== InventoryDocumentStatus::Draft->value) {
            return;
        }

        $item->loadMissing('product');
        if ($item->product === null || (int) ($order->warehouse_id ?? 0) <= 0) {
            return;
        }

        try {
            $sku = InventoryPostingProductSkuResolver::resolveSkuForPosting(
                $item->product,
                $item->product_sku_id !== null ? (int) $item->product_sku_id : null,
                'product_sku_id',
            );
        } catch (ValidationException) {
            return;
        }

        $bookScaled = $this->bookScaledIntegerForSku(
            (int) $order->warehouse_id,
            (int) $item->product_id,
            (int) $sku->getKey(),
        );

        $actualBaseString = (string) ($item->base_actual_quantity ?? $item->actual_quantity ?? '0');
        $actualScaled = $this->toScaledInteger($actualBaseString);
        $differenceScaled = $actualScaled - $bookScaled;

        $item->forceFill([
            'book_quantity' => $this->fromScaledInteger($bookScaled),
            'difference_quantity' => $this->fromScaledInteger($differenceScaled),
            'result_type' => $this->resultTypeForDifference($differenceScaled)->value,
        ])->saveQuietly();
    }

    public function apply(DberpStocktakeOrder $order): void
    {
        try {
            DB::transaction(function () use ($order): void {
                $locked = DberpStocktakeOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();
                $locked->loadMissing('warehouse');

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

                $this->validateScope($locked);

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

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

                $locked->loadMissing('warehouse');

                $preflightLines = [];

                foreach ($items as $item) {
                    $this->validateScopeProduct($locked, $item->product);
                    $this->stocktakeSerialNumberDifferenceService->validateStocktakeItemSerialNumbers($item);

                    $sku = InventoryPostingProductSkuResolver::resolveSkuForPosting(
                        $item->product,
                        $item->product_sku_id !== null ? (int) $item->product_sku_id : null,
                    );

                    $actualBaseString = (string) ($item->base_actual_quantity ?? $item->actual_quantity);
                    $actual = $this->toScaledInteger($actualBaseString);

                    if ($actual < 0) {
                        throw ValidationException::withMessages([
                            'actual_quantity' => __('dberp.errors.stocktake_actual_negative'),
                        ]);
                    }

                    $bookSum = number_format(
                        (float) DberpInventoryBalance::query()
                            ->where('product_id', $item->product_id)
                            ->where('warehouse_id', $locked->warehouse_id)
                            ->where('product_sku_id', $sku->getKey())
                            ->sum('realtime_quantity'),
                        4,
                        '.',
                        ''
                    );

                    $book = $this->toScaledInteger($bookSum);
                    $difference = $actual - $book;

                    if ($difference < 0) {
                        $preflightLines[] = [
                            'sku' => $sku,
                            'warehouse' => $locked->warehouse,
                            'quantity' => $this->fromScaledInteger(abs($difference)),
                        ];
                    }
                }

                if ($preflightLines !== []) {
                    $this->ledgerService->assertNoOutboundShortagesPreflight(
                        $preflightLines,
                        InventoryBusinessType::StocktakeLoss,
                        (string) $locked->code,
                    );
                }

                foreach ($items as $item) {
                    $transaction = $this->applyItem($locked, $item);
                    $this->stocktakeDifferenceService->syncFromStocktakeItem($locked, $item, $transaction);
                    $this->stocktakeSerialNumberDifferenceService->syncFromStocktakeItem($locked, $item);
                }

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

                $this->auditService->logSemanticSave(
                    'dberp.stocktake_order.applied',
                    $locked,
                    function () use ($locked): void {
                        $locked->forceFill([
                            'status' => InventoryDocumentStatus::Effective->value,
                            'effective_at' => now(),
                        ])->save();
                    },
                    $before,
                    ['status' => $locked->status, 'effective_at' => $locked->effective_at],
                );
            });
        } catch (InsufficientStockException $exception) {
            $this->auditInsufficientStock($exception);

            throw $exception->validationException();
        }
    }

    public function void(DberpStocktakeOrder $order): void
    {
        try {
            DB::transaction(function () use ($order): void {
                $locked = DberpStocktakeOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();
                $locked->loadMissing('warehouse');

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

                if (! $this->ruleService->allowsEffectiveInventoryVoid()) {
                    throw ValidationException::withMessages([
                        'status' => __('dberp.errors.inventory_void_policy_blocked'),
                    ]);
                }

                $rollbackInventoryEffects = $this->ruleService->allowVoidRollback();

                if ($rollbackInventoryEffects) {
                    $items = $locked->items()->with('product')->orderBy('sort_order')->orderBy('id')->get();

                    $preflightVoidLines = [];

                    foreach ($items as $item) {
                        $difference = $this->toScaledInteger((string) $item->difference_quantity);

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

                        $sku = InventoryPostingProductSkuResolver::resolveSkuForPosting(
                            $item->product,
                            $item->product_sku_id !== null ? (int) $item->product_sku_id : null,
                        );

                        $preflightVoidLines[] = [
                            'sku' => $sku,
                            'warehouse' => $locked->warehouse,
                            'quantity' => $this->fromScaledInteger($difference),
                        ];
                    }

                    if ($preflightVoidLines !== []) {
                        $this->ledgerService->assertNoOutboundShortagesPreflight(
                            $preflightVoidLines,
                            InventoryBusinessType::StocktakeGainVoid,
                            (string) $locked->code,
                        );
                    }

                    foreach ($items as $item) {
                        $difference = $this->toScaledInteger((string) $item->difference_quantity);

                        if ($difference === 0) {
                            continue;
                        }

                        $sku = InventoryPostingProductSkuResolver::resolveSkuForPosting(
                            $item->product,
                            $item->product_sku_id !== null ? (int) $item->product_sku_id : null,
                        );

                        $lineTrace = InventoryLedgerLineTrace::fromDocumentLine($item, 'actual_quantity');

                        if ($difference > 0) {
                            $this->ledgerService->outbound(
                                productSku: $sku,
                                warehouse: $locked->warehouse,
                                quantity: $this->fromScaledInteger($difference),
                                businessType: InventoryBusinessType::StocktakeGainVoid,
                                businessNumber: (string) $locked->code,
                                sourceType: $locked->getMorphClass(),
                                sourceId: (int) $locked->getKey(),
                                remark: $item->remark ?? $locked->remark,
                                businessTime: now(),
                                idempotencyKey: sprintf('stocktake_difference:%d:void:%d:reverse_gain', (int) $locked->getKey(), (int) $item->getKey()),
                                lineTrace: $lineTrace,
                            );
                        }

                        if ($difference < 0) {
                            $this->ledgerService->inbound(
                                productSku: $sku,
                                warehouse: $locked->warehouse,
                                quantity: $this->fromScaledInteger(abs($difference)),
                                businessType: InventoryBusinessType::StocktakeLossVoid,
                                businessNumber: (string) $locked->code,
                                sourceType: $locked->getMorphClass(),
                                sourceId: (int) $locked->getKey(),
                                remark: $item->remark ?? $locked->remark,
                                businessTime: now(),
                                idempotencyKey: sprintf('stocktake_difference:%d:void:%d:reverse_loss', (int) $locked->getKey(), (int) $item->getKey()),
                                lineTrace: $lineTrace,
                            );
                        }
                    }

                    $this->stocktakeDifferenceService->resolveVoidedOrder($locked);
                    $this->stocktakeSerialNumberDifferenceService->resolveVoidedOrder($locked);
                }

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

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

            throw $exception->validationException();
        }
    }

    private function applyItem(DberpStocktakeOrder $order, DberpStocktakeOrderItem $item): ?DberpInventoryTransaction
    {
        $this->validateScopeProduct($order, $item->product);

        $sku = InventoryPostingProductSkuResolver::resolveSkuForPosting(
            $item->product,
            $item->product_sku_id !== null ? (int) $item->product_sku_id : null,
        );

        $actualBaseString = (string) ($item->base_actual_quantity ?? $item->actual_quantity);
        $actual = $this->toScaledInteger($actualBaseString);

        if ($actual < 0) {
            throw ValidationException::withMessages([
                'actual_quantity' => __('dberp.errors.stocktake_actual_negative'),
            ]);
        }

        $book = $this->bookScaledIntegerForSku(
            (int) $order->warehouse_id,
            (int) $item->product_id,
            (int) $sku->getKey(),
        );
        $difference = $actual - $book;
        $resultType = $this->resultTypeForDifference($difference);
        $transaction = null;

        $lineTrace = $difference !== 0
            ? InventoryLedgerLineTrace::fromDocumentLine($item, 'actual_quantity')
            : null;

        if ($difference > 0) {
            $gainKey = sprintf('stocktake_difference:%d:confirm:%d:gain', (int) $order->getKey(), (int) $item->getKey());

            $post = $this->ledgerService->inbound(
                productSku: $sku,
                warehouse: $order->warehouse,
                quantity: $this->fromScaledInteger($difference),
                businessType: InventoryBusinessType::StocktakeGain,
                businessNumber: (string) $order->code,
                sourceType: $order->getMorphClass(),
                sourceId: (int) $order->getKey(),
                remark: $item->remark ?? $order->remark,
                businessTime: $order->business_time ?? now(),
                idempotencyKey: $gainKey,
                lineTrace: $lineTrace,
            );

            $transaction = $post->transaction;
        }

        if ($difference < 0) {
            $lossKey = sprintf('stocktake_difference:%d:confirm:%d:loss', (int) $order->getKey(), (int) $item->getKey());

            $post = $this->ledgerService->outbound(
                productSku: $sku,
                warehouse: $order->warehouse,
                quantity: $this->fromScaledInteger(abs($difference)),
                businessType: InventoryBusinessType::StocktakeLoss,
                businessNumber: (string) $order->code,
                sourceType: $order->getMorphClass(),
                sourceId: (int) $order->getKey(),
                remark: $item->remark ?? $order->remark,
                businessTime: $order->business_time ?? now(),
                idempotencyKey: $lossKey,
                lineTrace: $lineTrace,
            );

            $transaction = $post->transaction;
        }

        $item->forceFill([
            'book_quantity' => $this->fromScaledInteger($book),
            'difference_quantity' => $this->fromScaledInteger($difference),
            'result_type' => $resultType->value,
        ])->save();

        return $transaction;
    }

    private function validateScope(DberpStocktakeOrder $order): void
    {
        $order->assertScopeFieldsForSave();
    }

    private function validateScopeProduct(DberpStocktakeOrder $order, DberpProduct $product): void
    {
        $order->assertLineProductWithinScope($product);
    }

    private function auditInsufficientStock(InsufficientStockException $exception): void
    {
        $balanceQuery = DberpInventoryBalance::query()->where('warehouse_id', $exception->warehouseId);

        if ($exception->productSkuId !== null) {
            $balanceQuery->where('product_sku_id', $exception->productSkuId);
        } else {
            $balanceQuery->where('product_id', $exception->productId);
        }

        $balance = $balanceQuery->first();

        $this->auditService->log(
            'dberp.inventory.insufficient_stock_blocked',
            $balance,
            null,
            $exception->auditContext(),
        );
    }

    private function bookScaledIntegerForSku(int $warehouseId, int $productId, int $productSkuId): int
    {
        return $this->toScaledInteger($this->lineHintService->sumRealtimeQuantity(
            $warehouseId,
            $productId,
            $productSkuId,
        ));
    }

    private function resultTypeForDifference(int $differenceScaled): StocktakeResultType
    {
        if ($differenceScaled > 0) {
            return StocktakeResultType::Gain;
        }

        if ($differenceScaled < 0) {
            return StocktakeResultType::Loss;
        }

        return StocktakeResultType::Balanced;
    }

    private function toScaledInteger(string $quantity): int
    {
        return (int) round(((float) $quantity) * 10000);
    }

    private function fromScaledInteger(int $quantity): string
    {
        return number_format($quantity / 10000, 4, '.', '');
    }
}
