<?php

declare(strict_types=1);

namespace App\Business\Inventory;

use App\Business\Inventory\Enums\InventoryDocumentStatus;
use App\Business\Inventory\Enums\TransferDocumentStatus;
use App\Business\Product\DberpDocumentLineUnitService;
use App\Models\DberpOtherStockInOrder;
use App\Models\DberpOtherStockInOrderItem;
use App\Models\DberpOtherStockOutOrder;
use App\Models\DberpOtherStockOutOrderItem;
use App\Models\DberpProductSku;
use App\Models\DberpTransferOrder;
use App\Models\DberpTransferOrderItem;
use App\Support\DberpDocumentLineBulkProductAddFormFields;
use App\Support\DberpLineProductSkuFormFields;
use App\Support\DberpProductSkuSelectOptions;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

/**
 * 库存类手工单据明细批量添加（其他入/出库、调拨等）：仅草稿可写，批次/序列号须行内单独维护。
 */
final class InventoryDocumentManualLineBulkAddService
{
    /**
     * @param  list<int>  $productSkuIds
     * @return list<DberpOtherStockInOrderItem>
     */
    public function createOtherStockInItemsFromSkus(
        DberpOtherStockInOrder $order,
        array $productSkuIds,
        string $quantity,
        ?string $remark = null,
        bool $skipExistingSkuLines = false,
    ): array {
        return $this->createInventoryLineItems(
            order: $order,
            productSkuIds: $productSkuIds,
            quantity: $quantity,
            remark: $remark,
            skipExistingSkuLines: $skipExistingSkuLines,
            editable: fn (DberpOtherStockInOrder $locked): bool => $locked->status === InventoryDocumentStatus::Draft->value,
            notEditableMessageKey: 'dberp.errors.other_stock_in_not_draft',
            productSkuScope: DberpProductSkuSelectOptions::PRODUCT_SCOPE_INVENTORY,
            invalidSkuMessageKey: 'dberp.errors.bulk_add_product_sku_not_inventory',
            existingSkuIdsQuery: fn (DberpOtherStockInOrder $locked): array => $locked->items()
                ->whereNotNull('product_sku_id')
                ->pluck('product_sku_id')
                ->map(fn (mixed $id): int => (int) $id)
                ->all(),
            maxSortQuery: fn (DberpOtherStockInOrder $locked): int => (int) ($locked->items()->max('sort_order') ?? 0),
            createLine: function (DberpOtherStockInOrder $locked, array $payload): Model {
                return $locked->items()->create($payload);
            },
        );
    }

    /**
     * @param  list<int>  $productSkuIds
     * @return list<DberpOtherStockOutOrderItem>
     */
    public function createOtherStockOutItemsFromSkus(
        DberpOtherStockOutOrder $order,
        array $productSkuIds,
        string $quantity,
        ?string $remark = null,
        bool $skipExistingSkuLines = false,
    ): array {
        return $this->createInventoryLineItems(
            order: $order,
            productSkuIds: $productSkuIds,
            quantity: $quantity,
            remark: $remark,
            skipExistingSkuLines: $skipExistingSkuLines,
            editable: fn (DberpOtherStockOutOrder $locked): bool => $locked->status === InventoryDocumentStatus::Draft->value,
            notEditableMessageKey: 'dberp.errors.other_stock_out_not_draft',
            productSkuScope: DberpProductSkuSelectOptions::PRODUCT_SCOPE_INVENTORY,
            invalidSkuMessageKey: 'dberp.errors.bulk_add_product_sku_not_inventory',
            existingSkuIdsQuery: fn (DberpOtherStockOutOrder $locked): array => $locked->items()
                ->whereNotNull('product_sku_id')
                ->pluck('product_sku_id')
                ->map(fn (mixed $id): int => (int) $id)
                ->all(),
            maxSortQuery: fn (DberpOtherStockOutOrder $locked): int => (int) ($locked->items()->max('sort_order') ?? 0),
            createLine: function (DberpOtherStockOutOrder $locked, array $payload): Model {
                return $locked->items()->create($payload);
            },
        );
    }

    /**
     * @param  list<int>  $productSkuIds
     * @return list<DberpTransferOrderItem>
     */
    public function createTransferItemsFromSkus(
        DberpTransferOrder $order,
        array $productSkuIds,
        string $quantity,
        ?string $remark = null,
        bool $skipExistingSkuLines = false,
    ): array {
        return $this->createInventoryLineItems(
            order: $order,
            productSkuIds: $productSkuIds,
            quantity: $quantity,
            remark: $remark,
            skipExistingSkuLines: $skipExistingSkuLines,
            editable: fn (DberpTransferOrder $locked): bool => $locked->status === TransferDocumentStatus::Draft->value,
            notEditableMessageKey: 'dberp.errors.transfer_order_not_draft',
            productSkuScope: DberpProductSkuSelectOptions::PRODUCT_SCOPE_INVENTORY,
            invalidSkuMessageKey: 'dberp.errors.bulk_add_product_sku_not_inventory',
            existingSkuIdsQuery: fn (DberpTransferOrder $locked): array => $locked->items()
                ->whereNotNull('product_sku_id')
                ->pluck('product_sku_id')
                ->map(fn (mixed $id): int => (int) $id)
                ->all(),
            maxSortQuery: fn (DberpTransferOrder $locked): int => (int) ($locked->items()->max('sort_order') ?? 0),
            createLine: function (DberpTransferOrder $locked, array $payload): Model {
                return $locked->items()->create($payload);
            },
        );
    }

    /**
     * @template TOrder of DberpOtherStockInOrder|DberpOtherStockOutOrder|DberpTransferOrder
     *
     * @param  list<int>  $productSkuIds
     * @param  callable(TOrder): bool  $editable
     * @param  callable(TOrder): list<int>  $existingSkuIdsQuery
     * @param  callable(TOrder): int  $maxSortQuery
     * @param  callable(TOrder, array<string, mixed>): Model  $createLine
     * @return list<Model>
     */
    private function createInventoryLineItems(
        DberpOtherStockInOrder|DberpOtherStockOutOrder|DberpTransferOrder $order,
        array $productSkuIds,
        string $quantity,
        ?string $remark,
        bool $skipExistingSkuLines,
        callable $editable,
        string $notEditableMessageKey,
        string $productSkuScope,
        string $invalidSkuMessageKey,
        callable $existingSkuIdsQuery,
        callable $maxSortQuery,
        callable $createLine,
    ): 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,
            $editable,
            $notEditableMessageKey,
            $productSkuScope,
            $invalidSkuMessageKey,
            $existingSkuIdsQuery,
            $maxSortQuery,
            $createLine,
        ): array {
            $locked = $order->newQuery()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();

            if (! $editable($locked)) {
                throw ValidationException::withMessages([
                    'status' => __($notEditableMessageKey),
                ]);
            }

            $existingSkuIds = $skipExistingSkuLines ? $existingSkuIdsQuery($locked) : [];
            $nextSort = $maxSortQuery($locked);
            $created = [];

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

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

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

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

                $nextSort++;

                $payload = DberpLineProductSkuFormFields::finalizeLineSkuFormData([
                    'product_id' => (int) $product->getKey(),
                    'product_sku_id' => (int) $sku->getKey(),
                    'unit_id' => DberpDocumentLineUnitService::defaultUnitIdForProduct($product),
                    'quantity' => $formattedQuantity,
                    'remark' => $remark,
                    'sort_order' => $nextSort,
                ]);

                $created[] = $createLine($locked, $payload);
                $existingSkuIds[] = $skuId;
            }

            return $created;
        });
    }
}
