using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using Tornado3_2026Election.Domain;

namespace Tornado3_2026Election.Services;

public sealed class PreElectionHistoryService
{
    private const string RelativeAssetPath = @"Assets\Data\pre_election_history.json";

    private static readonly string[] SupportedElectionTypes =
    [
        "광역단체장",
        "교육감",
        "기초단체장"
    ];

    private static readonly JsonSerializerOptions SerializerOptions = new()
    {
        PropertyNameCaseInsensitive = true,
        AllowTrailingCommas = true,
        ReadCommentHandling = JsonCommentHandling.Skip
    };

    private static readonly IReadOnlyDictionary<string, string> RegionAliases =
        new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            ["서울"] = "서울",
            ["서울특별시"] = "서울",
            ["부산"] = "부산",
            ["부산광역시"] = "부산",
            ["대구"] = "대구",
            ["대구광역시"] = "대구",
            ["인천"] = "인천",
            ["인천광역시"] = "인천",
            ["광주"] = "광주",
            ["광주광역시"] = "광주",
            ["대전"] = "대전",
            ["대전광역시"] = "대전",
            ["울산"] = "울산",
            ["울산광역시"] = "울산",
            ["세종"] = "세종",
            ["세종특별자치시"] = "세종",
            ["경기"] = "경기",
            ["경기도"] = "경기",
            ["강원"] = "강원",
            ["강원도"] = "강원",
            ["강원특별자치도"] = "강원",
            ["충북"] = "충북",
            ["충청북도"] = "충북",
            ["충남"] = "충남",
            ["충청남도"] = "충남",
            ["전북"] = "전북",
            ["전라북도"] = "전북",
            ["전북특별자치도"] = "전북",
            ["전남"] = "전남",
            ["전라남도"] = "전남",
            ["경북"] = "경북",
            ["경상북도"] = "경북",
            ["경남"] = "경남",
            ["경상남도"] = "경남",
            ["제주"] = "제주",
            ["제주도"] = "제주",
            ["제주특별자치도"] = "제주"
        };

    private static readonly string[] RegionLabels = RegionAliases.Keys
        .Distinct(StringComparer.OrdinalIgnoreCase)
        .OrderByDescending(value => value.Length)
        .ToArray();

    private readonly LogService _logService;
    private readonly string? _assetPath;
    private PreElectionHistoryCatalog _catalog;
    private IReadOnlyDictionary<string, IReadOnlyList<PreElectionHistoryRecord>> _recordsByElectionType;
    private IReadOnlyDictionary<string, IReadOnlyDictionary<string, PreElectionHistoryRecord>> _recordsByLookupKey;

    public PreElectionHistoryService(LogService logService)
    {
        _logService = logService;
        _assetPath = ResolveAssetPath();
        _catalog = NormalizeCatalog(LoadCatalog(_assetPath));
        _recordsByElectionType = new Dictionary<string, IReadOnlyList<PreElectionHistoryRecord>>(StringComparer.Ordinal);
        _recordsByLookupKey = new Dictionary<string, IReadOnlyDictionary<string, PreElectionHistoryRecord>>(StringComparer.Ordinal);
        RebuildIndexes();
    }

    public void SaveRecord(PreElectionHistoryRecord record)
    {
        if (record is null)
        {
            throw new ArgumentNullException(nameof(record));
        }

        var normalizedRecord = NormalizeRecord(record);
        var canonicalElectionType = normalizedRecord.ElectionType;

        var records = _catalog.Records.ToList();
        var existingIndex = records.FindIndex(existingRecord =>
            string.Equals(existingRecord.ElectionType, canonicalElectionType, StringComparison.Ordinal) &&
            string.Equals(existingRecord.Key, normalizedRecord.Key, StringComparison.OrdinalIgnoreCase));

        if (existingIndex >= 0)
        {
            records[existingIndex] = normalizedRecord;
        }
        else
        {
            records.Add(normalizedRecord);
        }

        _catalog = new PreElectionHistoryCatalog
        {
            Metadata = new PreElectionHistoryMetadata
            {
                Version = _catalog.Metadata.Version,
                GeneratedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
                CoverageNotes = _catalog.Metadata.CoverageNotes.ToArray(),
                Sources = _catalog.Metadata.Sources.ToArray()
            },
            Records = records
                .OrderBy(existingRecord => Array.IndexOf(SupportedElectionTypes, NormalizeElectionType(existingRecord.ElectionType)))
                .ThenBy(existingRecord => existingRecord.DisplayName, StringComparer.Ordinal)
                .ToArray()
        };
        _catalog = NormalizeCatalog(_catalog);

        PersistCatalog();
        RebuildIndexes();
    }

    private static PreElectionHistoryCatalog NormalizeCatalog(PreElectionHistoryCatalog catalog)
    {
        var records = catalog.Records
            .Select(NormalizeRecord)
            .GroupBy(record => $"{record.ElectionType}|{record.Key}", StringComparer.OrdinalIgnoreCase)
            .Select(MergeRecordGroup)
            .OrderBy(record => Array.IndexOf(SupportedElectionTypes, record.ElectionType))
            .ThenBy(record => record.DisplayName, StringComparer.Ordinal)
            .ToArray();

        return new PreElectionHistoryCatalog
        {
            Metadata = catalog.Metadata,
            Records = records
        };
    }

    private static PreElectionHistoryRecord NormalizeRecord(PreElectionHistoryRecord record)
    {
        var canonicalElectionType = NormalizeElectionType(record.ElectionType);
        var regionName = record.RegionName ?? string.Empty;
        var regionKey = string.IsNullOrWhiteSpace(record.RegionKey)
            ? NormalizeRegionKey(regionName)
            : record.RegionKey.Trim();
        var districtName = record.DistrictName ?? string.Empty;
        var displayName = record.DisplayName ?? string.Empty;

        if (string.Equals(canonicalElectionType, "기초단체장", StringComparison.Ordinal))
        {
            regionKey = string.IsNullOrWhiteSpace(regionKey)
                ? ResolveRegionKey(regionName, districtName, [displayName, record.Key])
                : regionKey;
            districtName = NormalizeBasicDistrictDisplayName(districtName, displayName, regionName);
            var districtKey = NormalizeBasicDistrictToken(districtName);
            var recordKey = BuildBasicLookupKey(regionKey, districtKey);
            displayName = string.IsNullOrWhiteSpace(regionName) || string.IsNullOrWhiteSpace(districtName)
                ? displayName.Trim()
                : $"{regionName.Trim()} {districtName}";

            return new PreElectionHistoryRecord
            {
                ElectionType = canonicalElectionType,
                Key = string.IsNullOrWhiteSpace(recordKey) ? record.Key ?? string.Empty : recordKey,
                RegionKey = regionKey,
                RegionName = regionName,
                DistrictName = districtName,
                DisplayName = displayName,
                TurnoutHistory = SortTurnoutHistory(record.TurnoutHistory),
                WinnerHistory = SortWinnerHistory(record.WinnerHistory)
            };
        }

        return new PreElectionHistoryRecord
        {
            ElectionType = canonicalElectionType,
            Key = record.Key ?? string.Empty,
            RegionKey = regionKey,
            RegionName = regionName,
            DistrictName = districtName,
            DisplayName = displayName,
            TurnoutHistory = SortTurnoutHistory(record.TurnoutHistory),
            WinnerHistory = SortWinnerHistory(record.WinnerHistory)
        };
    }

    private static PreElectionHistoryRecord MergeRecordGroup(IGrouping<string, PreElectionHistoryRecord> records)
    {
        var primary = records
            .OrderBy(record => record.DisplayName.Contains('(') ? 1 : 0)
            .ThenByDescending(record => record.WinnerHistory.Count + record.TurnoutHistory.Count)
            .First();

        return new PreElectionHistoryRecord
        {
            ElectionType = primary.ElectionType,
            Key = primary.Key,
            RegionKey = primary.RegionKey,
            RegionName = primary.RegionName,
            DistrictName = primary.DistrictName,
            DisplayName = primary.DisplayName,
            TurnoutHistory = records
                .SelectMany(record => record.TurnoutHistory)
                .GroupBy(entry => entry.ElectionOrder)
                .Select(group => group.First())
                .OrderBy(entry => entry.ElectionOrder)
                .ToArray(),
            WinnerHistory = records
                .SelectMany(record => record.WinnerHistory)
                .GroupBy(entry => entry.ElectionOrder)
                .Select(group => group.First())
                .OrderBy(entry => entry.ElectionOrder)
                .ToArray()
        };
    }

    private static IReadOnlyList<PreElectionHistoricalTurnoutEntry> SortTurnoutHistory(
        IReadOnlyList<PreElectionHistoricalTurnoutEntry>? entries)
    {
        return (entries ?? Array.Empty<PreElectionHistoricalTurnoutEntry>())
            .OrderBy(entry => entry.ElectionOrder)
            .ToArray();
    }

    private static IReadOnlyList<PreElectionHistoricalWinnerEntry> SortWinnerHistory(
        IReadOnlyList<PreElectionHistoricalWinnerEntry>? entries)
    {
        return (entries ?? Array.Empty<PreElectionHistoricalWinnerEntry>())
            .OrderBy(entry => entry.ElectionOrder)
            .ToArray();
    }

    private void RebuildIndexes()
    {
        _recordsByElectionType = SupportedElectionTypes.ToDictionary(
            electionType => electionType,
            electionType => (IReadOnlyList<PreElectionHistoryRecord>)_catalog.Records
                .Where(record => string.Equals(record.ElectionType, electionType, StringComparison.Ordinal))
                .OrderBy(record => record.DisplayName, StringComparer.Ordinal)
                .ToArray(),
            StringComparer.Ordinal);
        _recordsByLookupKey = SupportedElectionTypes.ToDictionary(
            electionType => electionType,
            electionType => BuildLookupIndex(_recordsByElectionType[electionType], electionType),
            StringComparer.Ordinal);
    }

    public string GeneratedAt => _catalog.Metadata.GeneratedAt;

    public IReadOnlyList<string> CoverageNotes => _catalog.Metadata.CoverageNotes;

    public IReadOnlyList<PreElectionHistorySourceReference> Sources => _catalog.Metadata.Sources;

    public IReadOnlyList<string> GetSupportedElectionTypes() => SupportedElectionTypes;

    public int GetRecordCount(string electionType)
    {
        return _recordsByElectionType.TryGetValue(NormalizeElectionType(electionType), out var records)
            ? records.Count
            : 0;
    }

    public IReadOnlyList<PreElectionHistoryRecord> GetSelectionRecords(string electionType)
    {
        return _recordsByElectionType.TryGetValue(NormalizeElectionType(electionType), out var records)
            ? records
            : Array.Empty<PreElectionHistoryRecord>();
    }

    public string GetCoverageSummary(string electionType)
    {
        var canonicalElectionType = NormalizeElectionType(electionType);
        var recordCount = GetRecordCount(canonicalElectionType);
        if (recordCount == 0)
        {
            return "저장형 사전 데이터가 아직 없습니다.";
        }

        return canonicalElectionType switch
        {
            "광역단체장" => $"{GeneratedAt} 저장 / 전국 {recordCount}개 시도 / 투표율 2002~2022 / 당선자 1995~2022",
            "교육감" => $"{GeneratedAt} 저장 / 전국 {recordCount}개 시도 / 직선제 기준 2010~2022",
            "기초단체장" => $"{GeneratedAt} 저장 / 전국 {recordCount}개 선거구 / 공식 API 기준 2002~2022",
            _ => $"{GeneratedAt} 저장 / {recordCount}건"
        };
    }

    public string GetCoverageDetail(string electionType)
    {
        var canonicalElectionType = NormalizeElectionType(electionType);
        return canonicalElectionType switch
        {
            "광역단체장" => "광역단체장 당선자는 1회부터 8회까지, 투표율은 3회부터 8회까지 저장했습니다.",
            "교육감" => "교육감은 전국 직선제가 적용된 2010년부터 2022년까지 저장했습니다.",
            "기초단체장" => "기초단체장은 중앙선거관리위원회 OpenAPI 기준 2002년부터 2022년까지 저장했습니다. 구 단위로 분할된 시 지역은 동일 시 단위로 투표율을 합산했습니다.",
            _ => "저장형 사전 데이터 설명이 없습니다."
        };
    }

    public PreElectionHistoryRecord? ResolveHistory(string electionType, string? regionName, string? districtName)
    {
        var canonicalElectionType = NormalizeElectionType(electionType);
        if (!_recordsByLookupKey.TryGetValue(canonicalElectionType, out var records))
        {
            return null;
        }

        foreach (var key in BuildLookupKeys(canonicalElectionType, regionName, districtName))
        {
            if (records.TryGetValue(key, out var record))
            {
                return record;
            }
        }

        if (string.Equals(canonicalElectionType, SupportedElectionTypes[2], StringComparison.Ordinal))
        {
            return ResolveUniqueBasicDistrictHistory(canonicalElectionType, regionName, districtName);
        }

        return null;
    }

    private PreElectionHistoryRecord? ResolveUniqueBasicDistrictHistory(string electionType, string? regionName, string? districtName)
    {
        if (!_recordsByElectionType.TryGetValue(electionType, out var records))
        {
            return null;
        }

        var districtTokens = new[] { districtName, regionName }
            .Select(NormalizeBasicDistrictToken)
            .Where(value => !string.IsNullOrWhiteSpace(value))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();
        if (districtTokens.Length == 0)
        {
            return null;
        }

        var matches = records
            .Where(record => districtTokens.Contains(NormalizeBasicDistrictToken(record.DistrictName), StringComparer.OrdinalIgnoreCase) ||
                districtTokens.Contains(NormalizeBasicDistrictToken(record.DisplayName), StringComparer.OrdinalIgnoreCase))
            .GroupBy(record => record.Key, StringComparer.OrdinalIgnoreCase)
            .Select(group => group.First())
            .Take(2)
            .ToArray();

        return matches.Length == 1 ? matches[0] : null;
    }

    public static string NormalizeElectionType(string? electionType)
    {
        return electionType switch
        {
            "교육감" => "교육감",
            "기초단체장" => "기초단체장",
            _ => "광역단체장"
        };
    }

    public static string NormalizeRegionKey(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }

        return NormalizeRegionKeys(value).FirstOrDefault() ?? string.Empty;
    }

    private static IReadOnlyDictionary<string, PreElectionHistoryRecord> BuildLookupIndex(
        IReadOnlyList<PreElectionHistoryRecord> records,
        string electionType)
    {
        var index = new Dictionary<string, PreElectionHistoryRecord>(StringComparer.OrdinalIgnoreCase);
        foreach (var record in records)
        {
            foreach (var key in BuildLookupKeys(
                         electionType,
                         record.RegionName,
                         record.DistrictName,
                         record.DisplayName,
                         record.RegionKey,
                         record.Key))
            {
                if (!index.ContainsKey(key))
                {
                    index[key] = record;
                }
            }
        }

        return index;
    }

    private static IEnumerable<string> BuildLookupKeys(
        string electionType,
        string? regionName,
        string? districtName,
        params string?[] additionalValues)
    {
        electionType = NormalizeElectionType(electionType);
        var yielded = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        if (electionType == "기초단체장")
        {
            foreach (var additionalValue in additionalValues.Where(value => !string.IsNullOrWhiteSpace(value)))
            {
                var trimmed = additionalValue!.Trim();
                if (trimmed.Contains('|') && yielded.Add(trimmed))
                {
                    yield return trimmed;
                }
            }

            var regionKeys = ResolveRegionKeys(regionName, districtName, additionalValues);
            var districtKeys = new[]
            {
                NormalizeBasicDistrictToken(districtName),
                NormalizeBasicDistrictToken(regionName)
            }
            .Concat(additionalValues
                .Where(value => !string.IsNullOrWhiteSpace(value) && !value!.Contains('|'))
                .Select(NormalizeBasicDistrictToken))
            .Where(value => !string.IsNullOrWhiteSpace(value))
            .Distinct(StringComparer.OrdinalIgnoreCase);

            foreach (var regionKey in regionKeys)
            {
                foreach (var districtKey in districtKeys)
                {
                    var combined = BuildBasicLookupKey(regionKey, districtKey);
                    if (!string.IsNullOrWhiteSpace(combined) && yielded.Add(combined))
                    {
                        yield return combined;
                    }
                }
            }

            yield break;
        }

        var regionCandidates = new[]
        {
            NormalizeRegionKey(regionName),
            NormalizeRegionKey(districtName)
        }
        .Concat(additionalValues.Select(NormalizeRegionKey))
        .Where(value => !string.IsNullOrWhiteSpace(value))
        .Distinct(StringComparer.OrdinalIgnoreCase);

        foreach (var candidate in regionCandidates)
        {
            if (yielded.Add(candidate))
            {
                yield return candidate;
            }
        }
    }

    private static string ResolveRegionKey(string? regionName, string? districtName, IEnumerable<string?> additionalValues)
    {
        var candidates = new[] { regionName, districtName }.Concat(additionalValues);
        foreach (var candidate in candidates)
        {
            var normalized = NormalizeRegionKey(candidate);
            if (!string.IsNullOrWhiteSpace(normalized))
            {
                return normalized;
            }
        }

        return string.Empty;
    }

    private static IReadOnlyList<string> ResolveRegionKeys(string? regionName, string? districtName, IEnumerable<string?> additionalValues)
    {
        return new[] { regionName, districtName }
            .Concat(additionalValues)
            .SelectMany(NormalizeRegionKeys)
            .Where(value => !string.IsNullOrWhiteSpace(value))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    private static IEnumerable<string> NormalizeRegionKeys(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            yield break;
        }

        var trimmed = value.Trim();
        var yielded = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var regionLabel in RegionLabels)
        {
            if (trimmed.Contains(regionLabel, StringComparison.OrdinalIgnoreCase) &&
                RegionAliases.TryGetValue(regionLabel, out var normalized) &&
                yielded.Add(normalized))
            {
                yield return normalized;
            }
        }

        if (RegionAliases.TryGetValue(trimmed, out var exactMatch) && yielded.Add(exactMatch))
        {
            yield return exactMatch;
        }
    }

    private static string BuildBasicLookupKey(string regionKey, string districtKey)
    {
        if (string.IsNullOrWhiteSpace(regionKey) || string.IsNullOrWhiteSpace(districtKey))
        {
            return string.Empty;
        }

        return $"{regionKey}|{districtKey}";
    }

    private static string NormalizeBasicDistrictToken(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }

        var normalized = StripBasicDistrictDisambiguation(value.Trim());
        foreach (var regionLabel in RegionLabels)
        {
            normalized = RemoveLeadingRegionLabel(normalized, regionLabel);
        }

        normalized = normalized
            .Replace("구청장", "구", StringComparison.Ordinal)
            .Replace("군수", "군", StringComparison.Ordinal)
            .Replace("시장", "시", StringComparison.Ordinal)
            .Replace("교육감", string.Empty, StringComparison.Ordinal)
            .Replace("()", string.Empty, StringComparison.Ordinal)
            .Replace(" ", string.Empty, StringComparison.Ordinal)
            .Trim();

        return normalized;
    }

    private static string RemoveLeadingRegionLabel(string value, string regionLabel)
    {
        if (string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(regionLabel))
        {
            return value;
        }

        var trimmed = value.TrimStart();
        if (!trimmed.StartsWith(regionLabel, StringComparison.OrdinalIgnoreCase))
        {
            return value;
        }

        return trimmed[regionLabel.Length..].TrimStart();
    }

    private static string NormalizeBasicDistrictDisplayName(
        string? districtName,
        string? displayName,
        string? regionName)
    {
        var normalized = string.IsNullOrWhiteSpace(districtName)
            ? displayName?.Trim() ?? string.Empty
            : districtName.Trim();

        if (!string.IsNullOrWhiteSpace(regionName) &&
            normalized.StartsWith(regionName.Trim(), StringComparison.Ordinal))
        {
            normalized = normalized[regionName.Trim().Length..].Trim();
        }

        return StripBasicDistrictDisambiguation(normalized);
    }

    private static string StripBasicDistrictDisambiguation(string value)
    {
        var normalized = value.Trim();
        foreach (var regionLabel in RegionLabels)
        {
            normalized = normalized.Replace($"({regionLabel})", string.Empty, StringComparison.OrdinalIgnoreCase);
        }

        return normalized
            .Replace("()", string.Empty, StringComparison.Ordinal)
            .Trim();
    }

    private void PersistCatalog()
    {
        if (string.IsNullOrWhiteSpace(_assetPath))
        {
            throw new InvalidOperationException("사전 데이터 저장 경로를 찾지 못했습니다.");
        }

        var directoryPath = Path.GetDirectoryName(_assetPath);
        if (!string.IsNullOrWhiteSpace(directoryPath))
        {
            Directory.CreateDirectory(directoryPath);
        }

        var saveOptions = new JsonSerializerOptions(SerializerOptions)
        {
            WriteIndented = true
        };
        var json = JsonSerializer.Serialize(_catalog, saveOptions);
        File.WriteAllText(_assetPath, json, new UTF8Encoding(false));
    }

    private PreElectionHistoryCatalog LoadCatalog(string? assetPath)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(assetPath) || !File.Exists(assetPath))
            {
                _logService.Warning($"사전 데이터 파일을 찾지 못했습니다: {RelativeAssetPath}");
                return new PreElectionHistoryCatalog();
            }

            var json = File.ReadAllText(assetPath);
            return JsonSerializer.Deserialize<PreElectionHistoryCatalog>(json, SerializerOptions)
                ?? new PreElectionHistoryCatalog();
        }
        catch (Exception ex)
        {
            _logService.Warning($"사전 데이터 로드에 실패했습니다: {ex.Message}");
            return new PreElectionHistoryCatalog();
        }
    }

    private static string? ResolveAssetPath()
    {
        var current = new DirectoryInfo(AppContext.BaseDirectory);
        while (current is not null)
        {
            if (File.Exists(Path.Combine(current.FullName, "Tornado3_2026Election.csproj")))
            {
                return Path.Combine(current.FullName, RelativeAssetPath);
            }

            var repositoryProjectPath = Path.Combine(
                current.FullName,
                "Tornado3_2026Election",
                "Tornado3_2026Election.csproj");
            if (File.Exists(repositoryProjectPath))
            {
                return Path.Combine(current.FullName, "Tornado3_2026Election", RelativeAssetPath);
            }

            current = current.Parent;
        }

        var bundledPath = Path.Combine(AppContext.BaseDirectory, RelativeAssetPath);
        return File.Exists(bundledPath) ? bundledPath : null;
    }
}
