<?php

declare(strict_types=1);

namespace App\Business\MasterData;

use App\Business\MasterData\Enums\RegionLevel;
use App\Models\DberpRegion;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use RuntimeException;

final class DberpChinaRegionImportService
{
    public function __construct(
        private readonly DberpChinaRegionDatasetReader $datasetReader,
    ) {}

    private const DIRECT_MUNICIPALITY_CITY_GROUPS = [
        '110100' => ['parent' => '110000', 'name' => '市辖区'],
        '120100' => ['parent' => '120000', 'name' => '市辖区'],
        '310100' => ['parent' => '310000', 'name' => '市辖区'],
        '500100' => ['parent' => '500000', 'name' => '市辖区'],
        '500200' => ['parent' => '500000', 'name' => '县'],
    ];

    private const PROVINCE_DIRECT_CITY_GROUPS = [
        '419000' => ['parent' => '410000', 'name' => '省直辖县级行政单位'],
        '429000' => ['parent' => '420000', 'name' => '省直辖县级行政单位'],
        '469000' => ['parent' => '460000', 'name' => '省直辖县级行政单位'],
        '659000' => ['parent' => '650000', 'name' => '省直辖县级行政单位'],
    ];

    /**
     * @return array<string, int|string>
     */
    public function import(bool $reactivateDisabled = false): array
    {
        $countryCode = (string) config('dberp.regions.china_import.country_code', 'CN');
        $source = (string) config('dberp.regions.china_import.source', 'china_bundled');
        $dataFile = $this->datasetReader->resolveDataFilePath();
        $countryName = (string) config('dberp.regions.country_labels.'.$countryCode, '中国');

        $rows = $this->datasetReader->loadRows();
        $records = $this->buildRecords($rows, $countryCode, $countryName, $source);
        $regionCodes = array_column($records, 'region_code');

        $manualCollision = DberpRegion::query()
            ->whereIn('region_code', $regionCodes)
            ->where('source', DberpRegionSource::Manual)
            ->exists();

        // 导入仅按导入文件中的 region_code upsert；手工地区使用独立编码空间，正常不会进入 $records。
        // 若编码冲突则整单中止，避免误写手工行。
        if ($manualCollision) {
            throw new RuntimeException('导入范围内的系统地区编码与手工维护地区冲突，已中止导入以保护手工数据。');
        }

        $existingCodes = DberpRegion::query()
            ->whereIn('region_code', $regionCodes)
            ->pluck('region_code')
            ->all();

        $existingMap = array_fill_keys($existingCodes, true);
        $createdCount = 0;
        $reactivatedCount = 0;

        foreach ($records as $record) {
            if (! isset($existingMap[$record['region_code']])) {
                $createdCount++;
            }
        }

        if ($reactivateDisabled) {
            $reactivatedCount = DberpRegion::query()
                ->whereIn('region_code', $regionCodes)
                ->where('is_enabled', false)
                ->count();
        }

        DB::transaction(function () use ($records, $reactivateDisabled): void {
            $idMap = DberpRegion::query()
                ->whereIn('region_code', array_column($records, 'region_code'))
                ->pluck('id', 'region_code')
                ->all();

            foreach ($this->groupRecordsByDepth($records) as $depthRecords) {
                $payloads = [];

                foreach ($depthRecords as $record) {
                    $parentRegionCode = $record['parent_region_code'];
                    $payload = Arr::except($record, ['parent_region_code']);
                    $payload['parent_id'] = $parentRegionCode === null ? null : ($idMap[$parentRegionCode] ?? null);

                    $payloads[] = $payload;
                }

                DberpRegion::query()->upsert(
                    $payloads,
                    ['region_code'],
                    $this->updateColumns($reactivateDisabled),
                );

                $resolvedIds = DberpRegion::query()
                    ->whereIn('region_code', array_column($payloads, 'region_code'))
                    ->pluck('id', 'region_code')
                    ->all();

                $idMap = [...$idMap, ...$resolvedIds];
            }
        });

        $this->recomputeLeafFlagsForCountry($countryCode);

        return [
            'country_code' => $countryCode,
            'country_name' => $countryName,
            'source' => $source,
            'data_file' => $dataFile,
            'total' => count($records),
            'created' => $createdCount,
            'updated' => count($records) - $createdCount,
            'reactivated' => $reactivatedCount,
        ];
    }

    /**
     * @param  list<array{code: string, name: string}>  $rows
     * @return list<array<string, int|string|null|bool>>
     */
    private function buildRecords(array $rows, string $countryCode, string $countryName, string $source): array
    {
        $provinceRows = [];
        $cityRows = [];
        $districtRows = [];

        foreach ($rows as $row) {
            if ($this->isProvinceCode($row['code'])) {
                $provinceRows[$row['code']] = $row['name'];

                continue;
            }

            if ($this->isCityCode($row['code'])) {
                $cityRows[$row['code']] = [
                    'name' => $row['name'],
                    'parent' => $this->provinceCodeFromExternal($row['code']),
                ];

                continue;
            }

            $districtRows[$row['code']] = $row['name'];
        }

        foreach (array_replace(self::DIRECT_MUNICIPALITY_CITY_GROUPS, self::PROVINCE_DIRECT_CITY_GROUPS) as $cityCode => $definition) {
            $cityCode = (string) $cityCode;

            if (! isset($provinceRows[$definition['parent']]) || isset($cityRows[$cityCode])) {
                continue;
            }

            $cityRows[$cityCode] = [
                'name' => $definition['name'],
                'parent' => $definition['parent'],
            ];
        }

        $districtDefinitions = [];

        foreach ($districtRows as $districtCode => $districtName) {
            $districtCode = (string) $districtCode;
            $districtName = (string) $districtName;

            $parentCityCode = $this->resolveDistrictParentCityCode(
                $districtCode,
                array_map(static fn (int|string $cityCode): string => (string) $cityCode, array_keys($cityRows)),
            );

            $districtDefinitions[$districtCode] = [
                'name' => $districtName,
                'parent' => $parentCityCode,
            ];
        }

        $records = [];
        $context = [];

        $provinceCodes = array_map(static fn (int|string $provinceCode): string => (string) $provinceCode, array_keys($provinceRows));
        sort($provinceCodes);

        foreach ($provinceCodes as $index => $provinceCode) {
            $provinceRegionCode = $this->buildRegionCode($countryCode, $provinceCode);
            $cityChildren = array_filter(
                $cityRows,
                static fn (array $city): bool => $city['parent'] === $provinceCode,
            );

            $records[] = $this->makeRecord(
                countryCode: $countryCode,
                regionCode: $provinceRegionCode,
                externalCode: $provinceCode,
                name: $provinceRows[$provinceCode],
                level: RegionLevel::Province,
                depth: 1,
                parentRegionCode: null,
                parentPath: null,
                parentMergerName: $countryName,
                sortOrder: $index,
                isLeaf: $cityChildren === [],
                source: $source,
            );

            $context[$provinceRegionCode] = [
                'path' => $provinceRegionCode,
                'merger_name' => $countryName.'/'.$provinceRows[$provinceCode],
            ];
        }

        $cityGroups = [];

        foreach ($cityRows as $cityCode => $city) {
            $cityCode = (string) $cityCode;

            $cityGroups[(string) $city['parent']][$cityCode] = (string) $city['name'];
        }

        foreach ($cityGroups as $parentProvinceCode => $cities) {
            $parentProvinceCode = (string) $parentProvinceCode;
            ksort($cities);
            $parentProvinceRegionCode = $this->buildRegionCode($countryCode, $parentProvinceCode);

            foreach (array_values(array_keys($cities)) as $index => $cityCode) {
                $cityCode = (string) $cityCode;
                $cityRegionCode = $this->buildRegionCode($countryCode, $cityCode);
                $districtChildren = array_filter(
                    $districtDefinitions,
                    static fn (array $district): bool => $district['parent'] === $cityCode,
                );

                $records[] = $this->makeRecord(
                    countryCode: $countryCode,
                    regionCode: $cityRegionCode,
                    externalCode: $cityCode,
                    name: $cities[$cityCode],
                    level: RegionLevel::City,
                    depth: 2,
                    parentRegionCode: $parentProvinceRegionCode,
                    parentPath: $context[$parentProvinceRegionCode]['path'],
                    parentMergerName: $context[$parentProvinceRegionCode]['merger_name'],
                    sortOrder: $index,
                    isLeaf: $districtChildren === [],
                    source: $source,
                );

                $context[$cityRegionCode] = [
                    'path' => $context[$parentProvinceRegionCode]['path'].'/'.$cityRegionCode,
                    'merger_name' => $context[$parentProvinceRegionCode]['merger_name'].'/'.$cities[$cityCode],
                ];
            }
        }

        $districtGroups = [];

        foreach ($districtDefinitions as $districtCode => $district) {
            $districtCode = (string) $districtCode;

            $districtGroups[(string) $district['parent']][$districtCode] = (string) $district['name'];
        }

        foreach ($districtGroups as $parentCityCode => $districts) {
            $parentCityCode = (string) $parentCityCode;
            ksort($districts);
            $parentCityRegionCode = $this->buildRegionCode($countryCode, $parentCityCode);

            foreach (array_values(array_keys($districts)) as $index => $districtCode) {
                $districtCode = (string) $districtCode;
                $districtRegionCode = $this->buildRegionCode($countryCode, $districtCode);

                $records[] = $this->makeRecord(
                    countryCode: $countryCode,
                    regionCode: $districtRegionCode,
                    externalCode: $districtCode,
                    name: $districts[$districtCode],
                    level: RegionLevel::District,
                    depth: 3,
                    parentRegionCode: $parentCityRegionCode,
                    parentPath: $context[$parentCityRegionCode]['path'],
                    parentMergerName: $context[$parentCityRegionCode]['merger_name'],
                    sortOrder: $index,
                    isLeaf: true,
                    source: $source,
                );
            }
        }

        return $records;
    }

    private function isProvinceCode(string $code): bool
    {
        return str_ends_with($code, '0000');
    }

    private function isCityCode(string $code): bool
    {
        return ! $this->isProvinceCode($code) && str_ends_with($code, '00');
    }

    private function provinceCodeFromExternal(string $code): string
    {
        return substr($code, 0, 2).'0000';
    }

    /**
     * @param  list<string>  $cityCodes
     */
    private function resolveDistrictParentCityCode(string $districtCode, array $cityCodes): string
    {
        $cityCode = substr($districtCode, 0, 4).'00';

        if (in_array($cityCode, $cityCodes, true)) {
            return $cityCode;
        }

        $provincePrefix = substr($districtCode, 0, 2);

        return match (true) {
            in_array($provincePrefix.'0100', $cityCodes, true) && in_array($provincePrefix, ['11', '12', '31'], true) => $provincePrefix.'0100',
            $provincePrefix === '50' && in_array(((int) $districtCode) < 500200 ? '500100' : '500200', $cityCodes, true) => ((int) $districtCode) < 500200 ? '500100' : '500200',
            str_starts_with($districtCode, '419') && in_array('419000', $cityCodes, true) => '419000',
            str_starts_with($districtCode, '429') && in_array('429000', $cityCodes, true) => '429000',
            str_starts_with($districtCode, '469') && in_array('469000', $cityCodes, true) => '469000',
            str_starts_with($districtCode, '659') && in_array('659000', $cityCodes, true) => '659000',
            default => throw new RuntimeException('无法为地区代码 '.$districtCode.' 解析上级地市。'),
        };
    }

    private function buildRegionCode(string $countryCode, string $externalCode): string
    {
        return $countryCode.'-'.$externalCode;
    }

    /**
     * @return array<string, int|string|null|bool>
     */
    private function makeRecord(
        string $countryCode,
        string $regionCode,
        string $externalCode,
        string $name,
        RegionLevel $level,
        int $depth,
        ?string $parentRegionCode,
        ?string $parentPath,
        ?string $parentMergerName,
        int $sortOrder,
        bool $isLeaf,
        string $source,
    ): array {
        $now = now();
        $path = $parentPath === null ? $regionCode : $parentPath.'/'.$regionCode;
        $mergerName = $parentMergerName === null ? $name : $parentMergerName.'/'.$name;

        return [
            'parent_region_code' => $parentRegionCode,
            'country_code' => $countryCode,
            'region_code' => $regionCode,
            'external_code' => $externalCode,
            'name' => $name,
            'short_name' => null,
            'level' => $level->value,
            'depth' => $depth,
            'path' => $path,
            'merger_name' => $mergerName,
            'sort_order' => $sortOrder,
            'is_enabled' => true,
            'is_leaf' => $isLeaf,
            'source' => $source,
            'created_at' => $now,
            'updated_at' => $now,
        ];
    }

    /**
     * @param  list<array<string, int|string|null|bool>>  $records
     * @return list<list<array<string, int|string|null|bool>>>
     */
    private function groupRecordsByDepth(array $records): array
    {
        $grouped = [];

        foreach ($records as $record) {
            $grouped[(int) $record['depth']][] = $record;
        }

        ksort($grouped);

        return array_values($grouped);
    }

    /**
     * @return list<string>
     */
    private function updateColumns(bool $reactivateDisabled): array
    {
        $columns = [
            'parent_id',
            'country_code',
            'external_code',
            'name',
            'level',
            'depth',
            'path',
            'merger_name',
            'sort_order',
            'is_leaf',
            'source',
            'updated_at',
        ];

        if ($reactivateDisabled) {
            $columns[] = 'is_enabled';
        }

        return $columns;
    }

    private function recomputeLeafFlagsForCountry(string $countryCode): void
    {
        $parentIds = DberpRegion::query()
            ->where('country_code', $countryCode)
            ->whereNotNull('parent_id')
            ->distinct()
            ->pluck('parent_id')
            ->all();

        DberpRegion::query()
            ->where('country_code', $countryCode)
            ->update(['is_leaf' => true]);

        if ($parentIds === []) {
            return;
        }

        DberpRegion::query()
            ->where('country_code', $countryCode)
            ->whereIn('id', $parentIds)
            ->update(['is_leaf' => false]);
    }
}
