<?php

declare(strict_types=1);

/**
 * 本文件属于 DBErp 进销存系统。
 *
 * 版权所有 © 2026 北京珑大钜商科技有限公司。保留所有权利。
 *
 * 未经官方授权，不得删除、篡改或隐藏本文件中的版权声明、产品标识与来源信息。
 * 商业授权、官方更新与技术支持服务，请以珑大官方支持中心记录为准。
 *
 * @link https://support.loongdom.cn
 * @see  https://www.dberp.com.cn
 */

namespace App\Business\Purchase;

use App\Business\Product\DberpDocumentLineUnitService;
use App\Business\Purchase\Enums\PurchaseRequestSourceType;
use App\Business\Purchase\Enums\PurchaseRequestStatus;
use App\Business\Purchase\Enums\PurchaseRequestUrgencyLevel;
use App\Models\DberpProductSku;
use App\Models\DberpPurchaseRequest;
use App\Models\DberpPurchaseRequestItem;
use App\Models\DberpWarehouse;
use App\Services\DberpAuditService;
use App\Support\DberpAuditTrailGuard;
use App\Support\DberpDocumentLineBulkProductAddFormFields;
use App\Support\DberpLineProductSkuFormFields;
use App\Support\DberpProductSkuSelectOptions;
use App\Support\DberpSystemProfileBusinessDefaults;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

final class PurchaseRequestService
{
    public function __construct(
        private readonly DberpAuditService $auditService,
    ) {}

    /**
     * 批量手工添加采购申请明细：须指定仓库，不写转单占用（与单行新建一致）。
     *
     * @param  list<int>  $productSkuIds
     * @return list<DberpPurchaseRequestItem>
     */
    public function createManualItemsFromSkus(
        DberpPurchaseRequest $request,
        array $productSkuIds,
        int $warehouseId,
        string $requestQuantity,
        ?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'),
            ]);
        }

        if ($warehouseId <= 0 || ! DberpWarehouse::query()->whereKey($warehouseId)->where('is_enabled', true)->exists()) {
            throw ValidationException::withMessages([
                DberpDocumentLineBulkProductAddFormFields::FIELD_WAREHOUSE_ID => (string) __('dberp.errors.bulk_add_warehouse_required'),
            ]);
        }

        $formattedQuantity = number_format((float) $requestQuantity, 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 ($request, $productSkuIds, $warehouseId, $formattedQuantity, $remark, $skipExistingSkuLines): array {
            $lockedRequest = DberpPurchaseRequest::query()->whereKey($request->getKey())->lockForUpdate()->firstOrFail();

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

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

            $nextSort = (int) ($lockedRequest->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++;

                $payload = DberpLineProductSkuFormFields::finalizeLineSkuFormData([
                    'product_id' => (int) $product->getKey(),
                    'product_sku_id' => (int) $sku->getKey(),
                    'unit_id' => DberpDocumentLineUnitService::defaultUnitIdForProduct($product),
                    'warehouse_id' => $warehouseId,
                    'source_type' => DberpSystemProfileBusinessDefaults::defaultPurchaseRequestSourceType(),
                    'request_quantity' => $formattedQuantity,
                    'converted_quantity' => '0.0000',
                    'remark' => $remark,
                    'sort_order' => $nextSort,
                    'external_product_id' => $product->external_product_id,
                    'external_sku' => $product->external_sku,
                ]);

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

            return $created;
        });
    }

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

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

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

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

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

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

    public function submit(DberpPurchaseRequest $request): void
    {
        DB::transaction(function () use ($request): void {
            $locked = DberpPurchaseRequest::query()->with('items')->whereKey($request->getKey())->lockForUpdate()->firstOrFail();

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

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

            $this->refreshTotals($locked);

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

            $this->auditService->logSemanticSave(
                'dberp.purchase_request.submitted',
                $locked,
                function () use ($locked): void {
                    $locked->forceFill([
                        'status' => PurchaseRequestStatus::Pending->value,
                        'submitted_at' => now(),
                    ])->save();
                },
                $before,
                ['status' => $locked->status, 'submitted_at' => $locked->submitted_at],
            );
        });
    }

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

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

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

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

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

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

            if ((float) $locked->converted_quantity > 0) {
                throw ValidationException::withMessages([
                    'status' => __('dberp.errors.purchase_request_has_converted_items'),
                ]);
            }

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

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

    public function refreshTotals(DberpPurchaseRequest $request): void
    {
        $totals = $request->items()
            ->selectRaw('COALESCE(SUM(request_quantity), 0) as total_quantity, COALESCE(SUM(converted_quantity), 0) as converted_quantity, COALESCE(SUM(remaining_quantity), 0) as remaining_quantity')
            ->first();

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

    public function syncProgressStatus(DberpPurchaseRequest $request): void
    {
        $locked = DberpPurchaseRequest::query()->whereKey($request->getKey())->lockForUpdate()->firstOrFail();

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

        $totalQuantity = (float) $locked->total_quantity;
        $convertedQuantity = (float) $locked->converted_quantity;
        $remainingQuantity = (float) $locked->remaining_quantity;

        $targetStatus = $locked->status === PurchaseRequestStatus::Draft->value
            ? PurchaseRequestStatus::Draft
            : PurchaseRequestStatus::Pending;

        if ($totalQuantity > 0 && $remainingQuantity <= 0.00005) {
            $targetStatus = PurchaseRequestStatus::Converted;
        } elseif ($convertedQuantity > 0.00005) {
            $targetStatus = PurchaseRequestStatus::PartiallyConverted;
        } elseif ($locked->status !== PurchaseRequestStatus::Draft->value) {
            $targetStatus = PurchaseRequestStatus::Pending;
        }

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

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

    /**
     * @param  array<string, mixed>  $data
     * @return array<string, mixed>
     */
    private function normalizePayload(array $data, ?DberpPurchaseRequest $request = null): array
    {
        return [
            'code' => trim((string) Arr::get($data, 'code', $request?->code ?? '')),
            'request_time' => Arr::get($data, 'request_time', $request?->request_time ?? now()),
            'source_type' => Arr::get($data, 'source_type', $request?->source_type ?? PurchaseRequestSourceType::Manual->value),
            'source_system' => trim((string) Arr::get($data, 'source_system', $request?->source_system ?? 'dberp')) ?: 'dberp',
            'source_id' => Arr::get($data, 'source_id', $request?->source_id),
            'source_reference_no' => Arr::get($data, 'source_reference_no', $request?->source_reference_no),
            'external_reference_id' => trim((string) Arr::get($data, 'external_reference_id', $request?->external_reference_id ?? '')) ?: null,
            'source_snapshot' => Arr::get($data, 'source_snapshot', $request?->source_snapshot),
            'department_name' => trim((string) Arr::get($data, 'department_name', $request?->department_name ?? '')) ?: null,
            'applicant_employee_id' => Arr::get($data, 'applicant_employee_id', $request?->applicant_employee_id),
            'suggested_supplier_id' => Arr::get($data, 'suggested_supplier_id', $request?->suggested_supplier_id),
            'urgency_level' => Arr::get($data, 'urgency_level', $request?->urgency_level ?? PurchaseRequestUrgencyLevel::Normal->value),
            'expected_arrival_date' => Arr::get($data, 'expected_arrival_date', $request?->expected_arrival_date),
            'request_reason' => trim((string) Arr::get($data, 'request_reason', $request?->request_reason ?? '')) ?: null,
            'status' => Arr::get($data, 'status', $request?->status ?? PurchaseRequestStatus::Draft->value),
            'submitted_at' => Arr::get($data, 'submitted_at', $request?->submitted_at),
            'last_converted_at' => Arr::get($data, 'last_converted_at', $request?->last_converted_at),
            'closed_at' => Arr::get($data, 'closed_at', $request?->closed_at),
            'voided_at' => Arr::get($data, 'voided_at', $request?->voided_at),
            'remark' => trim((string) Arr::get($data, 'remark', $request?->remark ?? '')) ?: null,
        ];
    }
}
