<?php

declare(strict_types=1);

namespace App\Business\DataImport\Handlers;

use App\Business\DataImport\Contracts\DataImportHandler;
use App\Business\DataImport\DataImportExecutionResult;
use App\Business\DataImport\DataImportPrecheckResult;
use App\Business\DataImport\DataImportRow;
use App\Business\DataImport\DataImportRowPrecheckOutcome;
use App\Business\DataImport\Enums\DataImportRowStatus;
use App\Business\DataImport\Enums\DataImportType;
use App\Business\DataImport\OpeningStockImportService;
use App\Business\DataImport\Support\DataImportReferenceResolver;
use App\Business\DataImport\Support\DataImportValueParser;
use App\Business\Inventory\InventorySerialNumberService;
use App\Models\DberpDataImportBatch;
use App\Models\DberpInventorySerialNumber;
use App\Models\DberpProduct;
use App\Models\DberpProductSku;

final class OpeningStockImportHandler extends AbstractMasterDataImportHandler implements DataImportHandler
{
    public function __construct(
        private readonly OpeningStockImportService $importService,
        private readonly DataImportReferenceResolver $referenceResolver,
        private readonly InventorySerialNumberService $serialNumberService,
    ) {}

    public function importType(): DataImportType
    {
        return DataImportType::OpeningStock;
    }

    /**
     * @param  list<DataImportRow>  $rows
     */
    public function precheckRows(array $rows): DataImportPrecheckResult
    {
        $outcomes = $this->buildInitialOutcomes($rows);
        $productCodesInFile = [];
        $warehouseCodesInFile = [];
        $externalKeysInFile = [];

        foreach ($rows as $row) {
            $productCode = DataImportValueParser::trimmed($row->value('product_code'));
            $warehouseCode = DataImportValueParser::trimmed($row->value('warehouse_code'));
            $externalKey = DataImportValueParser::trimmed($row->value('external_key'));
            $errors = [];

            if ($productCode === null) {
                $errors[] = DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.data_import.validation.product_code_required'));
            } else {
                $productCodesInFile[] = $productCode;
            }

            if ($warehouseCode === null) {
                $errors[] = DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.data_import.validation.warehouse_code_required'));
            } else {
                $warehouseCodesInFile[] = $warehouseCode;
            }

            if ($externalKey !== null) {
                $externalKeysInFile[] = $externalKey;
            }

            try {
                DataImportValueParser::parsePositiveDecimal($row->value('quantity'));
            } catch (\InvalidArgumentException $exception) {
                $errors[] = DataImportValueParser::rowError($row->rowNumber, $exception->getMessage());
            }

            try {
                DataImportValueParser::parseNonNegativeDecimal($row->value('unit_cost'), null);
            } catch (\InvalidArgumentException $exception) {
                $errors[] = DataImportValueParser::rowError($row->rowNumber, $exception->getMessage());
            }

            try {
                DataImportValueParser::parseNonNegativeDecimal($row->value('safety_stock'), null);
            } catch (\InvalidArgumentException $exception) {
                $errors[] = DataImportValueParser::rowError($row->rowNumber, $exception->getMessage());
            }

            try {
                DataImportValueParser::parseOptionalDate($row->value('production_date'));
                DataImportValueParser::parseOptionalDate($row->value('expiry_date'));
                DataImportValueParser::parseOptionalDateTime($row->value('business_time'));
            } catch (\InvalidArgumentException $exception) {
                $errors[] = DataImportValueParser::rowError($row->rowNumber, $exception->getMessage());
            }

            if ($errors !== []) {
                $outcomes = $this->markRowFailed($outcomes, $row->rowNumber, $errors);
            }
        }

        $outcomes = $this->applyDuplicateFieldErrors(
            $rows,
            $outcomes,
            'external_key',
            'dberp.data_import.validation.external_key_duplicate_in_file',
        );

        $productsByCode = $this->referenceResolver->enabledInventoryProductsByCodes(array_values(array_unique($productCodesInFile)));
        $existingProductCodes = $this->referenceResolver->existingProductCodes(array_values(array_unique($productCodesInFile)));
        $warehousesByCode = $this->referenceResolver->enabledWarehousesByCodes(array_values(array_unique($warehouseCodesInFile)));
        $existingWarehouseCodes = $this->referenceResolver->existingWarehouseCodes(array_values(array_unique($warehouseCodesInFile)));
        $importedExternalKeys = $this->referenceResolver->importedOpeningStockExternalKeys(array_values(array_unique($externalKeysInFile)));

        $inventoryRowGroups = [];

        foreach ($rows as $row) {
            $productCode = DataImportValueParser::trimmed($row->value('product_code'));
            $warehouseCode = DataImportValueParser::trimmed($row->value('warehouse_code'));
            $skuCode = DataImportValueParser::trimmed($row->value('sku_code'));
            $externalKey = DataImportValueParser::trimmed($row->value('external_key'));
            $batchNo = DataImportValueParser::trimmed($row->value('batch_no'));
            $serialNo = DataImportValueParser::trimmed($row->value('serial_no'));
            $expiryDate = DataImportValueParser::trimmed($row->value('expiry_date'));
            $errors = [];

            if ($productCode !== null && ! in_array($productCode, $existingProductCodes, true)) {
                $errors[] = DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.data_import.validation.invalid_product_code', [
                    'value' => $productCode,
                ]));
            } elseif ($productCode !== null && ! isset($productsByCode[$productCode])) {
                $errors[] = DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.data_import.validation.disabled_or_non_inventory_product_code', [
                    'value' => $productCode,
                ]));
            }

            if ($warehouseCode !== null && ! in_array($warehouseCode, $existingWarehouseCodes, true)) {
                $errors[] = DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.data_import.validation.invalid_warehouse_code', [
                    'value' => $warehouseCode,
                ]));
            } elseif ($warehouseCode !== null && ! isset($warehousesByCode[$warehouseCode])) {
                $errors[] = DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.data_import.validation.disabled_warehouse_code', [
                    'value' => $warehouseCode,
                ]));
            }

            if ($externalKey !== null && in_array($externalKey, $importedExternalKeys, true)) {
                $errors[] = DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.data_import.validation.external_key_already_imported', [
                    'value' => $externalKey,
                ]));
            }

            $product = $productCode !== null ? ($productsByCode[$productCode] ?? null) : null;
            $sku = $product instanceof DberpProduct
                ? $this->importService->resolveSku($product, $skuCode)
                : null;

            if ($product instanceof DberpProduct) {
                if ($skuCode === null && ! $sku instanceof DberpProductSku) {
                    $errors[] = DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.data_import.validation.product_sku_code_required'));
                } elseif ($skuCode !== null && ! $sku instanceof DberpProductSku) {
                    $errors[] = DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.data_import.validation.invalid_product_sku_code', [
                        'value' => $skuCode,
                    ]));
                } else {
                    $errors = array_merge($errors, $this->validateInventoryRefinementFields($row, $product, $serialNo, $batchNo, $expiryDate));
                }
            }

            if ($productCode !== null && $warehouseCode !== null) {
                $inventoryKey = $this->inventoryRowKey($productCode, $skuCode, $warehouseCode, $externalKey);
                $inventoryRowGroups[$inventoryKey][] = $row->rowNumber;
            }

            if ($errors !== []) {
                $outcomes = $this->markRowFailed($outcomes, $row->rowNumber, $errors);
            }
        }

        foreach ($inventoryRowGroups as $rowNumbers) {
            if (count($rowNumbers) <= 1) {
                continue;
            }

            foreach ($rowNumbers as $rowNumber) {
                $outcomes = $this->markRowFailed($outcomes, $rowNumber, [
                    DataImportValueParser::rowError($rowNumber, (string) __('dberp.data_import.validation.opening_stock_duplicate_inventory_row')),
                ]);
            }
        }

        return $this->buildPrecheckResult($this->finalizeOutcomes($outcomes));
    }

    public function execute(DberpDataImportBatch $batch): DataImportExecutionResult
    {
        $outcomes = [];
        $importedCount = 0;
        $failedCount = 0;

        $validatedRows = $batch->rows()
            ->where('status', DataImportRowStatus::Validated)
            ->orderBy('row_number')
            ->get();

        foreach ($validatedRows as $batchRow) {
            $outcome = $this->importService->importRow($batch, $batchRow);
            $outcomes[] = $outcome;

            if ($outcome->imported()) {
                $importedCount++;
            } else {
                $failedCount++;
            }
        }

        return new DataImportExecutionResult($outcomes, $importedCount, $failedCount);
    }

    /**
     * @return list<string>
     */
    private function validateInventoryRefinementFields(
        DataImportRow $row,
        DberpProduct $product,
        ?string $serialNo,
        ?string $batchNo,
        ?string $expiryDate,
    ): array {
        $errors = [];
        $serials = $this->serialNumberService->normalizeSerialNumbers($serialNo);

        if ($product->manages_batch && $batchNo === null) {
            $errors[] = DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.data_import.validation.batch_no_required'));
        }

        if ($product->manages_serial_number) {
            if ($serials === []) {
                $errors[] = DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.errors.inventory_serial_number_required'));
            } else {
                $existingSerials = DberpInventorySerialNumber::query()
                    ->whereIn('serial_no', $serials)
                    ->pluck('serial_no')
                    ->map(static fn ($value): string => (string) $value)
                    ->all();

                foreach ($existingSerials as $existingSerial) {
                    $errors[] = DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.errors.inventory_serial_number_exists'));
                }

                try {
                    $quantity = DataImportValueParser::parsePositiveDecimal($row->value('quantity'));
                } catch (\InvalidArgumentException) {
                    return $errors;
                }

                if (! preg_match('/^\d+$/', $quantity)) {
                    $errors[] = DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.data_import.validation.serial_product_requires_integer_quantity'));
                } elseif (count($serials) !== (int) $quantity) {
                    $errors[] = DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.data_import.validation.serial_count_must_match_quantity'));
                }
            }
        }

        if ($product->manages_expiry && $expiryDate === null) {
            $errors[] = DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.data_import.validation.expiry_date_required'));
        }

        return $errors;
    }

    private function inventoryRowKey(string $productCode, ?string $skuCode, string $warehouseCode, ?string $externalKey): string
    {
        return implode('|', [
            $productCode,
            $skuCode ?? '',
            $warehouseCode,
            $externalKey ?? '',
        ]);
    }

    /**
     * @param  list<DataImportRowPrecheckOutcome>  $outcomes
     */
    private function buildPrecheckResult(array $outcomes): DataImportPrecheckResult
    {
        $validatedCount = 0;
        $failedCount = 0;

        foreach ($outcomes as $outcome) {
            if ($outcome->passed()) {
                $validatedCount++;
            } else {
                $failedCount++;
            }
        }

        return new DataImportPrecheckResult($outcomes, $validatedCount, $failedCount);
    }
}
