<?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\DataImportRowExecutionOutcome;
use App\Business\DataImport\DataImportRowPrecheckOutcome;
use App\Business\DataImport\Enums\DataImportRowStatus;
use App\Business\DataImport\Enums\DataImportType;
use App\Business\DataImport\Support\DataImportValueParser;
use App\Business\DataImport\Support\ProductCategoryImportGraph;
use App\Models\DberpDataImportBatch;
use App\Models\DberpDataImportBatchRow;
use App\Models\DberpProductCategory;
use Illuminate\Support\Facades\DB;

final class ProductCategoryImportHandler extends AbstractMasterDataImportHandler implements DataImportHandler
{
    public function __construct(
        private readonly ProductCategoryImportGraph $categoryGraph,
    ) {}

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

    /**
     * @param  list<DataImportRow>  $rows
     */
    public function precheckRows(array $rows): DataImportPrecheckResult
    {
        $outcomes = $this->buildInitialOutcomes($rows);
        $codesInFile = [];
        /** @var array<string, string|null> $codeToParentCode */
        $codeToParentCode = [];

        foreach ($rows as $row) {
            $name = DataImportValueParser::trimmed($row->value('name'));
            $code = DataImportValueParser::trimmed($row->value('code'));
            $parentCode = DataImportValueParser::trimmed($row->value('parent_code'));
            $errors = [];

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

            if ($code !== null) {
                $codesInFile[] = $code;
                $codeToParentCode[$code] = $parentCode;
            }

            if ($code !== null && $parentCode !== null && $code === $parentCode) {
                $errors[] = DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.data_import.validation.self_parent_reference'));
            }

            try {
                DataImportValueParser::parseBoolean($row->value('is_enabled'), true);
                DataImportValueParser::parseSortOrder($row->value('sort_order'), 0);
            } catch (\InvalidArgumentException $exception) {
                $errors[] = DataImportValueParser::rowError($row->rowNumber, $exception->getMessage());
            }

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

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

        $existingCodes = $this->loadExistingValues(
            DberpProductCategory::class,
            'code',
            array_values(array_unique($codesInFile)),
        );

        $allExistingCategoryCodes = DberpProductCategory::query()
            ->whereNotNull('code')
            ->pluck('code')
            ->map(static fn ($value): string => (string) $value)
            ->all();

        foreach ($rows as $row) {
            $code = DataImportValueParser::trimmed($row->value('code'));
            $parentCode = DataImportValueParser::trimmed($row->value('parent_code'));
            $errors = [];

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

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

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

        $cycleCodes = $this->categoryGraph->detectCycleCodes($codeToParentCode, $allExistingCategoryCodes);

        foreach ($rows as $row) {
            $code = DataImportValueParser::trimmed($row->value('code'));

            if ($code !== null && in_array($code, $cycleCodes, true)) {
                $outcomes = $this->markRowFailed($outcomes, $row->rowNumber, [
                    DataImportValueParser::rowError($row->rowNumber, (string) __('dberp.data_import.validation.circular_parent_reference')),
                ]);
            }
        }

        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();

        /** @var array<string, int> $codeToId */
        $codeToId = DberpProductCategory::query()
            ->whereNotNull('code')
            ->pluck('id', 'code')
            ->mapWithKeys(static fn ($id, $code): array => [(string) $code => (int) $id])
            ->all();

        /** @var array<string, string|null> $codeToParentCode */
        $codeToParentCode = [];

        foreach ($validatedRows as $batchRow) {
            $snapshot = is_array($batchRow->raw_snapshot) ? $batchRow->raw_snapshot : [];
            $code = DataImportValueParser::trimmed($snapshot['code'] ?? null);

            if ($code !== null) {
                $codeToParentCode[$code] = DataImportValueParser::trimmed($snapshot['parent_code'] ?? null);
            }
        }

        $existingCodes = array_keys($codeToId);

        try {
            $orderedCodes = $this->categoryGraph->topologicalOrder($codeToParentCode, $existingCodes);
        } catch (\RuntimeException $exception) {
            foreach ($validatedRows as $batchRow) {
                $message = DataImportValueParser::rowError((int) $batchRow->row_number, $exception->getMessage());
                $batchRow->update([
                    'status' => DataImportRowStatus::Failed,
                    'error_messages' => [$message],
                ]);

                $outcomes[] = new DataImportRowExecutionOutcome(
                    rowNumber: (int) $batchRow->row_number,
                    status: DataImportRowStatus::Failed,
                    errorMessages: [$message],
                );
                $failedCount++;
            }

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

        /** @var array<string, DberpDataImportBatchRow> $rowsByCode */
        $rowsByCode = [];

        foreach ($validatedRows as $batchRow) {
            $snapshot = is_array($batchRow->raw_snapshot) ? $batchRow->raw_snapshot : [];
            $code = DataImportValueParser::trimmed($snapshot['code'] ?? null);

            if ($code !== null) {
                $rowsByCode[$code] = $batchRow;
            }
        }

        /** @var list<DberpDataImportBatchRow> $rowsWithoutCode */
        $rowsWithoutCode = [];

        foreach ($validatedRows as $batchRow) {
            $snapshot = is_array($batchRow->raw_snapshot) ? $batchRow->raw_snapshot : [];
            $code = DataImportValueParser::trimmed($snapshot['code'] ?? null);

            if ($code === null) {
                $rowsWithoutCode[] = $batchRow;
            }
        }

        DB::transaction(function () use ($orderedCodes, $rowsByCode, $rowsWithoutCode, &$codeToId, &$outcomes, &$importedCount, &$failedCount): void {
            foreach ($orderedCodes as $code) {
                if (! isset($rowsByCode[$code])) {
                    continue;
                }

                $outcome = $this->importCategoryRow($rowsByCode[$code], $codeToId);
                $outcomes[] = $outcome;

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

            foreach ($rowsWithoutCode as $batchRow) {
                $outcome = $this->importCategoryRow($batchRow, $codeToId);
                $outcomes[] = $outcome;

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

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

    /**
     * @param  array<string, int>  $codeToId
     */
    private function importCategoryRow(DberpDataImportBatchRow $batchRow, array &$codeToId): DataImportRowExecutionOutcome
    {
        $snapshot = is_array($batchRow->raw_snapshot) ? $batchRow->raw_snapshot : [];
        $parentCode = DataImportValueParser::trimmed($snapshot['parent_code'] ?? null);
        $parentId = null;

        if ($parentCode !== null) {
            if (! isset($codeToId[$parentCode])) {
                $message = DataImportValueParser::rowError((int) $batchRow->row_number, (string) __('dberp.data_import.validation.invalid_parent_code', [
                    'value' => $parentCode,
                ]));

                $batchRow->update([
                    'status' => DataImportRowStatus::Failed,
                    'error_messages' => [$message],
                ]);

                return new DataImportRowExecutionOutcome(
                    rowNumber: (int) $batchRow->row_number,
                    status: DataImportRowStatus::Failed,
                    errorMessages: [$message],
                );
            }

            $parentId = $codeToId[$parentCode];
        }

        try {
            $category = DberpProductCategory::query()->create([
                'code' => DataImportValueParser::trimmed($snapshot['code'] ?? null),
                'name' => (string) DataImportValueParser::trimmed($snapshot['name'] ?? null),
                'parent_id' => $parentId,
                'is_enabled' => DataImportValueParser::parseBoolean($snapshot['is_enabled'] ?? null, true),
                'sort_order' => DataImportValueParser::parseSortOrder($snapshot['sort_order'] ?? null, 0),
                'remark' => DataImportValueParser::trimmed($snapshot['remark'] ?? null),
            ]);

            $code = DataImportValueParser::trimmed($snapshot['code'] ?? null) ?? (string) $category->code;

            if ($code !== '') {
                $codeToId[$code] = (int) $category->getKey();
            }

            $batchRow->update([
                'status' => DataImportRowStatus::Imported,
                'created_record_type' => DberpProductCategory::class,
                'created_record_id' => (int) $category->getKey(),
                'error_messages' => null,
            ]);

            return new DataImportRowExecutionOutcome(
                rowNumber: (int) $batchRow->row_number,
                status: DataImportRowStatus::Imported,
                createdRecordType: DberpProductCategory::class,
                createdRecordId: (int) $category->getKey(),
            );
        } catch (\Throwable $exception) {
            $message = DataImportValueParser::rowError((int) $batchRow->row_number, (string) __('dberp.data_import.validation.import_failed', [
                'reason' => $exception->getMessage(),
            ]));

            $batchRow->update([
                'status' => DataImportRowStatus::Failed,
                'error_messages' => [$message],
            ]);

            return new DataImportRowExecutionOutcome(
                rowNumber: (int) $batchRow->row_number,
                status: DataImportRowStatus::Failed,
                errorMessages: [$message],
            );
        }
    }

    /**
     * @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);
    }
}
