using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using KAsyncEngineLib;

namespace Tornado3_2026Election.Services;

internal static class PartyColorCatalog
{
    private const int DefaultBarWidth = 140;
    private const int DefaultBarHeight = 174;
    private const int DefaultPlateWidth = 552;
    private const int DefaultPlateHeight = 736;
    private const int DefaultSymbolWidth = 31;
    private const int DefaultSymbolHeight = 30;

    private static readonly Regex StyleTargetPattern = new(@"(?<target>face|edge|shadow|underline|frame)(?:\s*(?<order>\d+)번째)?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
    private static readonly Regex RgbRowPattern = new(@"^(?<party>.+?)\s+(?<r>\d{1,3})\s+(?<g>\d{1,3})\s+(?<b>\d{1,3})\s*$", RegexOptions.Compiled);
    private static readonly Regex CleanupPattern = new(@"[^\p{L}\p{Nd}]+", RegexOptions.Compiled);
    private static readonly Regex InvalidFileNamePattern = new(@"[^\p{L}\p{Nd}_-]+", RegexOptions.Compiled);
    private static readonly ConcurrentDictionary<string, CachedCatalog> CatalogCache = new(StringComparer.OrdinalIgnoreCase);
    private static readonly IReadOnlyDictionary<string, string> ExplicitRgbSpecMap = BuildExplicitRgbSpecMap();
    private static readonly string[] OtherPartyFallbackKeys =
    [
        "무기타",
        "무소속기타",
        "기타",
        "무소속"
    ];

    public static string ResolveFallbackAssetPath(
        string templateFolderPath,
        string templateName,
        string partyName,
        PartyColorAssetUsage usage)
    {
        var catalog = LoadCatalog(templateFolderPath, templateName);
        if (catalog is null || !TryResolveColor(catalog, partyName, usage, out var sectionName, out var color))
        {
            return string.Empty;
        }

        return GenerateSolidColorPng(templateName, partyName, usage, sectionName, color);
    }

    public static string ResolveFallbackAssetPathForSection(
        string templateFolderPath,
        string templateName,
        string sectionName,
        string partyName,
        PartyColorAssetUsage usage)
    {
        var catalog = LoadCatalog(templateFolderPath, templateName);
        if (catalog is null ||
            string.IsNullOrWhiteSpace(sectionName) ||
            string.IsNullOrWhiteSpace(partyName) ||
            !catalog.Sections.TryGetValue(NormalizeSectionKey(sectionName), out var section))
        {
            return string.Empty;
        }

        foreach (var candidatePartyName in GetPartyKeyCandidates(partyName).Concat(OtherPartyFallbackKeys))
        {
            if (section.PartyColors.TryGetValue(candidatePartyName, out var color))
            {
                return GenerateSolidColorPng(templateName, partyName, usage, section.DisplayName, color);
            }
        }

        return string.Empty;
    }

    public static bool HasStyleColorBinding(string templateFolderPath, string templateName, string sectionName)
    {
        var catalog = LoadCatalog(templateFolderPath, templateName);
        return catalog is not null &&
               catalog.Sections.TryGetValue(NormalizeSectionKey(sectionName), out var section) &&
               section.StyleBinding is not null;
    }

    public static bool TryResolveStyleColor(
        string templateFolderPath,
        string templateName,
        string sectionName,
        string partyName,
        out PartyStyleColorSpec styleColor)
    {
        styleColor = default;

        var catalog = LoadCatalog(templateFolderPath, templateName);
        if (catalog is null ||
            !catalog.Sections.TryGetValue(NormalizeSectionKey(sectionName), out var section) ||
            section.StyleBinding is null)
        {
            return false;
        }

        foreach (var candidatePartyName in GetPartyKeyCandidates(partyName).Concat(OtherPartyFallbackKeys))
        {
            if (!section.PartyColors.TryGetValue(candidatePartyName, out var color))
            {
                continue;
            }

            styleColor = new PartyStyleColorSpec(
                section.StyleBinding.StyleType,
                section.StyleBinding.Order,
                color.R,
                color.G,
                color.B,
                byte.MaxValue);
            return true;
        }

        return false;
    }

    private static TemplatePartyColorCatalog? LoadCatalog(string templateFolderPath, string templateName)
    {
        var rgbFilePath = ResolveRgbSpecPath(templateFolderPath, templateName);
        if (string.IsNullOrWhiteSpace(rgbFilePath) || !File.Exists(rgbFilePath))
        {
            return null;
        }

        var lastWriteTimeUtc = File.GetLastWriteTimeUtc(rgbFilePath);
        if (CatalogCache.TryGetValue(rgbFilePath, out var cached) &&
            cached.LastWriteTimeUtc == lastWriteTimeUtc)
        {
            return cached.Catalog;
        }

        var parsed = ParseCatalog(rgbFilePath);
        CatalogCache[rgbFilePath] = new CachedCatalog(lastWriteTimeUtc, parsed);
        return parsed;
    }

    private static string? ResolveRgbSpecPath(string templateFolderPath, string templateName)
    {
        if (string.IsNullOrWhiteSpace(templateFolderPath))
        {
            return null;
        }

        var rgbDirectoryPath = Path.Combine(templateFolderPath, "RGB");
        if (!Directory.Exists(rgbDirectoryPath))
        {
            return null;
        }

        var folderName = Path.GetFileName(Path.GetFullPath(templateFolderPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
        if (TryGetExplicitRgbSpecBaseName(folderName, templateName, out var explicitSpecBaseName) &&
            string.IsNullOrWhiteSpace(explicitSpecBaseName))
        {
            return null;
        }

        if (!string.IsNullOrWhiteSpace(explicitSpecBaseName))
        {
            var explicitSpecPath = Path.Combine(rgbDirectoryPath, explicitSpecBaseName + ".txt");
            if (File.Exists(explicitSpecPath))
            {
                return explicitSpecPath;
            }
        }

        var exactPath = Path.Combine(rgbDirectoryPath, templateName + ".txt");
        if (File.Exists(exactPath))
        {
            return exactPath;
        }

        var candidates = Directory.GetFiles(rgbDirectoryPath, "*.txt");
        if (candidates.Length == 0)
        {
            return null;
        }

        return candidates
            .Select(path => new
            {
                Path = path,
                Score = ScoreTemplateMatch(templateName, Path.GetFileNameWithoutExtension(path))
            })
            .Where(candidate => candidate.Score > 0)
            .OrderByDescending(candidate => candidate.Score)
            .ThenBy(candidate => candidate.Path, StringComparer.OrdinalIgnoreCase)
            .FirstOrDefault()
            ?.Path;
    }

    private static bool TryGetExplicitRgbSpecBaseName(string folderName, string templateName, out string? specBaseName)
    {
        return ExplicitRgbSpecMap.TryGetValue(BuildExplicitMapKey(folderName, templateName), out specBaseName);
    }

    private static int ScoreTemplateMatch(string templateName, string fileBaseName)
    {
        if (string.Equals(templateName, fileBaseName, StringComparison.OrdinalIgnoreCase))
        {
            return 1000;
        }

        var normalizedTemplateName = NormalizeTemplateName(templateName);
        var normalizedFileBaseName = NormalizeTemplateName(fileBaseName);
        if (string.Equals(normalizedTemplateName, normalizedFileBaseName, StringComparison.Ordinal))
        {
            return 900;
        }

        var score = 0;
        if (normalizedFileBaseName.Contains(normalizedTemplateName, StringComparison.Ordinal) ||
            normalizedTemplateName.Contains(normalizedFileBaseName, StringComparison.Ordinal))
        {
            score += 300;
        }

        var templateTokens = GetTemplateTokens(templateName);
        var fileTokens = GetTemplateTokens(fileBaseName);
        foreach (var token in templateTokens)
        {
            if (fileTokens.Contains(token))
            {
                score += 100;
            }
        }

        return score;
    }

    private static HashSet<string> GetTemplateTokens(string value)
    {
        return value
            .Split(['_', ',', ' ', '(', ')'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Select(token => CleanupPattern.Replace(token, string.Empty))
            .Where(token => !string.IsNullOrWhiteSpace(token))
            .ToHashSet(StringComparer.OrdinalIgnoreCase);
    }

    private static string NormalizeTemplateName(string value)
    {
        return CleanupPattern.Replace(value, string.Empty);
    }

    private static string BuildExplicitMapKey(string folderName, string templateName)
    {
        return $"{folderName}|{templateName}";
    }

    private static TemplatePartyColorCatalog ParseCatalog(string rgbFilePath)
    {
        var sections = new Dictionary<string, SectionBuilder>(StringComparer.OrdinalIgnoreCase);
        var headerBuilder = new StringBuilder();
        List<SectionHeaderEntry>? currentSectionHeaders = null;
        var inHeader = false;

        foreach (var rawLine in File.ReadLines(rgbFilePath, Encoding.UTF8))
        {
            var line = rawLine.Trim();
            if (string.IsNullOrWhiteSpace(line))
            {
                continue;
            }

            if (line.StartsWith("(", StringComparison.Ordinal))
            {
                if (inHeader)
                {
                    currentSectionHeaders = ExtractSectionHeaders(headerBuilder.ToString());
                }

                headerBuilder.Clear();
                headerBuilder.AppendLine(line);
                inHeader = !line.Contains(')');
                if (!inHeader)
                {
                    currentSectionHeaders = ExtractSectionHeaders(headerBuilder.ToString());
                }

                continue;
            }

            if (inHeader)
            {
                if (IsHeaderContinuationLine(line))
                {
                    headerBuilder.AppendLine(line);
                    if (line.Contains(')'))
                    {
                        inHeader = false;
                        currentSectionHeaders = ExtractSectionHeaders(headerBuilder.ToString());
                    }

                    continue;
                }

                inHeader = false;
                currentSectionHeaders = ExtractSectionHeaders(headerBuilder.ToString());
            }

            if (currentSectionHeaders is null || currentSectionHeaders.Count == 0 || line.StartsWith("R", StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            if (!TryParseRgbRow(line, out var partyName, out var color))
            {
                continue;
            }

            foreach (var sectionHeader in currentSectionHeaders)
            {
                var sectionKey = NormalizeSectionKey(sectionHeader.SectionName);
                if (!sections.TryGetValue(sectionKey, out var sectionBuilder))
                {
                    sectionBuilder = new SectionBuilder(sectionHeader.SectionName, sectionHeader.StyleBinding);
                    sections[sectionKey] = sectionBuilder;
                }
                else if (sectionBuilder.StyleBinding is null && sectionHeader.StyleBinding is not null)
                {
                    sectionBuilder.StyleBinding = sectionHeader.StyleBinding;
                }

                sectionBuilder.PartyColors[NormalizePartyKey(partyName)] = color;
            }
        }

        return new TemplatePartyColorCatalog(
            sections.ToDictionary(
                pair => pair.Key,
                pair => new PartyColorSection(
                    pair.Value.DisplayName,
                    pair.Value.StyleBinding,
                    pair.Value.PartyColors.ToDictionary(
                        colorPair => colorPair.Key,
                        colorPair => colorPair.Value,
                        StringComparer.OrdinalIgnoreCase)),
                StringComparer.OrdinalIgnoreCase));
    }

    private static bool IsHeaderContinuationLine(string line)
    {
        if (line.Contains(')'))
        {
            return true;
        }

        var normalizedLine = line.Trim().Trim('(', ')').Trim();
        return TryParseSectionHeaderLine(normalizedLine, out _, out _);
    }

    private static List<SectionHeaderEntry> ExtractSectionHeaders(string header)
    {
        var entries = new List<SectionHeaderEntry>();
        foreach (var rawLine in header.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries))
        {
            var line = rawLine.Trim().Trim('(', ')').Trim();
            if (string.IsNullOrWhiteSpace(line))
            {
                continue;
            }

            if (!TryParseSectionHeaderLine(line, out var sectionNames, out var styleBinding))
            {
                continue;
            }

            foreach (var sectionName in sectionNames)
            {
                var normalizedSectionName = NormalizeSectionKey(sectionName);
                if (entries.Any(existing => string.Equals(existing.SectionKey, normalizedSectionName, StringComparison.OrdinalIgnoreCase)))
                {
                    continue;
                }

                entries.Add(new SectionHeaderEntry(sectionName, normalizedSectionName, styleBinding));
            }
        }

        return entries;
    }

    private static bool TryParseSectionHeaderLine(
        string line,
        out IReadOnlyList<string> sectionNames,
        out PartyStyleBinding? styleBinding)
    {
        sectionNames = Array.Empty<string>();
        styleBinding = null;

        var colonIndex = line.IndexOf(':');
        var firstArrowIndex = line.IndexOf('>');
        if (colonIndex < 0 && firstArrowIndex < 0)
        {
            return false;
        }

        string sectionsPart;
        string bindingPart;
        if (colonIndex >= 0 && (firstArrowIndex < 0 || colonIndex < firstArrowIndex))
        {
            sectionsPart = line[..colonIndex];
            bindingPart = line[(colonIndex + 1)..];
        }
        else
        {
            sectionsPart = line[..firstArrowIndex];
            bindingPart = line[(firstArrowIndex + 1)..];
        }

        var parsedSections = sectionsPart
            .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Select(name => name.Trim())
            .Where(name => !string.IsNullOrWhiteSpace(name))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();
        if (parsedSections.Length == 0)
        {
            return false;
        }

        styleBinding = TryParseStyleBinding(bindingPart);
        sectionNames = parsedSections;
        return true;
    }

    private static PartyStyleBinding? TryParseStyleBinding(string bindingPart)
    {
        var tokens = bindingPart
            .Split('>', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Select(token => token.Trim())
            .Where(token => !string.IsNullOrWhiteSpace(token))
            .ToArray();
        if (tokens.Length == 0 ||
            !tokens.Any(token => token.Contains("color", StringComparison.OrdinalIgnoreCase)))
        {
            return null;
        }

        var styleToken = tokens.FirstOrDefault(token => StyleTargetPattern.IsMatch(token));
        if (styleToken is null)
        {
            return null;
        }

        var match = StyleTargetPattern.Match(styleToken);
        if (!match.Success)
        {
            return null;
        }

        var styleType = match.Groups["target"].Value.ToLowerInvariant() switch
        {
            "face" => eKStyleType.STYLE_TYPE_FACE,
            "edge" => eKStyleType.STYLE_TYPE_EDGE,
            "shadow" => eKStyleType.STYLE_TYPE_SHADOW,
            "underline" => eKStyleType.STYLE_TYPE_UNDERLINE,
            "frame" => eKStyleType.STYLE_TYPE_FRAME,
            _ => (eKStyleType?)null
        };
        if (styleType is null)
        {
            return null;
        }

        var order = 0;
        if (int.TryParse(match.Groups["order"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedOrder) &&
            parsedOrder > 1)
        {
            order = parsedOrder - 1;
        }

        return new PartyStyleBinding(styleType.Value, order);
    }

    private static bool TryParseRgbRow(string line, out string partyName, out PartyRgbColor color)
    {
        partyName = string.Empty;
        color = default;

        var match = RgbRowPattern.Match(line);
        if (!match.Success)
        {
            return false;
        }

        if (!byte.TryParse(match.Groups["r"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var r) ||
            !byte.TryParse(match.Groups["g"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var g) ||
            !byte.TryParse(match.Groups["b"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var b))
        {
            return false;
        }

        partyName = match.Groups["party"].Value.Trim();
        color = new PartyRgbColor(r, g, b);
        return !string.IsNullOrWhiteSpace(partyName);
    }

    private static bool TryResolveColor(
        TemplatePartyColorCatalog catalog,
        string partyName,
        PartyColorAssetUsage usage,
        out string sectionName,
        out PartyRgbColor color)
    {
        foreach (var candidateSectionName in GetSectionCandidates(usage))
        {
            if (!catalog.Sections.TryGetValue(NormalizeSectionKey(candidateSectionName), out var section))
            {
                continue;
            }

            foreach (var candidatePartyName in GetPartyKeyCandidates(partyName).Concat(OtherPartyFallbackKeys))
            {
                if (section.PartyColors.TryGetValue(candidatePartyName, out color))
                {
                    sectionName = section.DisplayName;
                    return true;
                }
            }
        }

        sectionName = string.Empty;
        color = default;
        return false;
    }

    private static IEnumerable<string> GetSectionCandidates(PartyColorAssetUsage usage)
    {
        return usage switch
        {
            PartyColorAssetUsage.Bar => ["정당바", "정당판"],
            PartyColorAssetUsage.Plate => ["정당판", "정당바"],
            PartyColorAssetUsage.Outline => ["정당원", "정당색", "정당판", "정당바"],
            PartyColorAssetUsage.Color => ["정당색", "정당바", "정당판"],
            PartyColorAssetUsage.Group => ["그룹", "정당바", "정당판"],
            PartyColorAssetUsage.Symbol => ["정당심볼", "정당바", "정당판"],
            _ => Array.Empty<string>()
        };
    }

    private static IEnumerable<string> GetPartyKeyCandidates(string partyName)
    {
        var trimmed = partyName.Trim();
        if (string.IsNullOrWhiteSpace(trimmed))
        {
            yield break;
        }

        yield return NormalizePartyKey(trimmed);

        var noSpaces = NormalizePartyKey(trimmed.Replace(" ", string.Empty));
        if (!string.Equals(noSpaces, NormalizePartyKey(trimmed), StringComparison.OrdinalIgnoreCase))
        {
            yield return noSpaces;
        }

        if (trimmed.Contains("무소속", StringComparison.Ordinal) ||
            trimmed.Contains("무기타", StringComparison.Ordinal) ||
            trimmed.Contains("기타", StringComparison.Ordinal))
        {
            yield return "무기타";
        }

        foreach (var alias in GetHistoricalPartyAliases(trimmed))
        {
            yield return alias;
        }
    }

    private static string NormalizePartyKey(string partyName)
    {
        return partyName.Trim().Replace(" ", string.Empty);
    }

    private static IEnumerable<string> GetHistoricalPartyAliases(string partyName)
    {
        var normalized = NormalizePartyKey(partyName);
        if (string.IsNullOrWhiteSpace(normalized))
        {
            yield break;
        }

        if (normalized is "국힘" or "미래통합당" or "민주자유당" or "신한국당" or "한나라당" or "새누리당" or "자유한국당")
        {
            yield return "국민의힘";
        }

        if (normalized is "민주당" or "민주당1991" or "민주당2000" or "민주당2008" or "새정치국민회의" or "새천년민주당" or "열린민주당" or "열린우리당" or "민주통합당" or "새정치민주연합")
        {
            yield return "더불어민주당";
        }

        if (normalized is "무소속")
        {
            yield return "무기타";
            yield return "무소속기타";
        }
    }

    private static string NormalizeSectionKey(string sectionName)
    {
        return sectionName.Trim().Replace(" ", string.Empty);
    }

    private static string GenerateSolidColorPng(
        string templateName,
        string partyName,
        PartyColorAssetUsage usage,
        string sectionName,
        PartyRgbColor color)
    {
        var cacheDirectory = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "Tornado3_2026Election",
            "GeneratedPartyAssets");
        Directory.CreateDirectory(cacheDirectory);

        var safeTemplateName = SanitizeFileName(templateName);
        var safePartyName = SanitizeFileName(partyName);
        var safeSectionName = SanitizeFileName(sectionName);
        var (width, height) = usage switch
        {
            PartyColorAssetUsage.Symbol => (DefaultSymbolWidth, DefaultSymbolHeight),
            PartyColorAssetUsage.Bar or PartyColorAssetUsage.Group => (DefaultBarWidth, DefaultBarHeight),
            _ => (DefaultPlateWidth, DefaultPlateHeight)
        };
        var fileName = $"{safeTemplateName}_{usage}_{safeSectionName}_{safePartyName}_{color.R}_{color.G}_{color.B}_{width}x{height}.png";
        var filePath = Path.Combine(cacheDirectory, fileName);
        if (File.Exists(filePath))
        {
            return filePath;
        }

        WriteSolidColorPng(filePath, width, height, color);
        return filePath;
    }

    private static string SanitizeFileName(string value)
    {
        var sanitized = InvalidFileNamePattern.Replace(value, "_").Trim('_');
        return string.IsNullOrWhiteSpace(sanitized) ? "party" : sanitized;
    }

    private static void WriteSolidColorPng(string filePath, int width, int height, PartyRgbColor color)
    {
        var rawBytes = new byte[height * (1 + (width * 4))];
        var index = 0;
        for (var y = 0; y < height; y++)
        {
            rawBytes[index++] = 0;
            for (var x = 0; x < width; x++)
            {
                rawBytes[index++] = color.R;
                rawBytes[index++] = color.G;
                rawBytes[index++] = color.B;
                rawBytes[index++] = byte.MaxValue;
            }
        }

        using var compressedStream = new MemoryStream();
        using (var zlibStream = new ZLibStream(compressedStream, CompressionLevel.SmallestSize, leaveOpen: true))
        {
            zlibStream.Write(rawBytes, 0, rawBytes.Length);
        }

        using var outputStream = File.Create(filePath);
        outputStream.Write(new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 });

        Span<byte> headerBytes =
        [
            0, 0, 0, 0,
            0, 0, 0, 0,
            8,
            6,
            0,
            0,
            0
        ];
        WriteBigEndian(width, headerBytes[..4]);
        WriteBigEndian(height, headerBytes.Slice(4, 4));
        WriteChunk(outputStream, "IHDR", headerBytes.ToArray());
        WriteChunk(outputStream, "IDAT", compressedStream.ToArray());
        WriteChunk(outputStream, "IEND", Array.Empty<byte>());
    }

    private static void WriteChunk(Stream outputStream, string chunkType, byte[] data)
    {
        Span<byte> lengthBytes = stackalloc byte[4];
        WriteBigEndian(data.Length, lengthBytes);
        outputStream.Write(lengthBytes);

        var chunkTypeBytes = Encoding.ASCII.GetBytes(chunkType);
        outputStream.Write(chunkTypeBytes, 0, chunkTypeBytes.Length);
        outputStream.Write(data, 0, data.Length);

        var crc = ComputeCrc32(chunkTypeBytes, data);
        Span<byte> crcBytes = stackalloc byte[4];
        WriteBigEndian(unchecked((int)crc), crcBytes);
        outputStream.Write(crcBytes);
    }

    private static void WriteBigEndian(int value, Span<byte> bytes)
    {
        bytes[0] = (byte)((value >> 24) & 255);
        bytes[1] = (byte)((value >> 16) & 255);
        bytes[2] = (byte)((value >> 8) & 255);
        bytes[3] = (byte)(value & 255);
    }

    private static uint ComputeCrc32(byte[] chunkTypeBytes, byte[] data)
    {
        var crc = 4294967295u;
        Update(chunkTypeBytes);
        Update(data);
        return ~crc;

        void Update(byte[] bytes)
        {
            foreach (var value in bytes)
            {
                crc ^= value;
                for (var bit = 0; bit < 8; bit++)
                {
                    crc = (crc & 1) != 0
                        ? 3988292384u ^ (crc >> 1)
                        : crc >> 1;
                }
            }
        }
    }

    private sealed record CachedCatalog(DateTime LastWriteTimeUtc, TemplatePartyColorCatalog Catalog);
    private sealed record TemplatePartyColorCatalog(IReadOnlyDictionary<string, PartyColorSection> Sections);
    private sealed record PartyColorSection(
        string DisplayName,
        PartyStyleBinding? StyleBinding,
        IReadOnlyDictionary<string, PartyRgbColor> PartyColors);
    private sealed record SectionHeaderEntry(string SectionName, string SectionKey, PartyStyleBinding? StyleBinding);
    private sealed class SectionBuilder
    {
        public SectionBuilder(string displayName, PartyStyleBinding? styleBinding)
        {
            DisplayName = displayName;
            StyleBinding = styleBinding;
        }

        public string DisplayName { get; }

        public PartyStyleBinding? StyleBinding { get; set; }

        public Dictionary<string, PartyRgbColor> PartyColors { get; } = new(StringComparer.OrdinalIgnoreCase);
    }

    private sealed record PartyStyleBinding(eKStyleType StyleType, int Order);
    private readonly record struct PartyRgbColor(byte R, byte G, byte B);

    private static IReadOnlyDictionary<string, string> BuildExplicitRgbSpecMap()
    {
        var mappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

        static void Add(
            IDictionary<string, string> target,
            string folderName,
            string specBaseName,
            params string[] templateNames)
        {
            foreach (var templateName in templateNames)
            {
                target[BuildExplicitMapKey(folderName, templateName)] = specBaseName;
            }
        }

        Add(
            mappings,
            "Elect2026_Normal_민방",
            "1-2위_ani_광역단체장",
            "1-2위_ani_광역단체장");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "1-2위_ani_기초단체장",
            "1-2위_ani_기초단체장",
            "1-2위_ani_기초단체장_5760",
            "1-2위_ani_기초단체장_L");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "1-2위_광역단체장, 보궐",
            "1-2위_광역단체장",
            "1-2위_광역단체장_5760",
            "1-2위_광역단체장_L",
            "1-2위_보궐선거");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "1-2위_광역단체장,기초단체장_시도별영상",
            "1-2위_광역단체장_시도별영상",
            "1-2위_기초단체장_시도별영상");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "1-2위_교육감",
            "1-2위_교육감");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "1-2위_기초단체장",
            "1-2위_기초단체장");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "1-3위_ani_광역단체장,보궐",
            "1-3위_ani_광역단체장",
            "1-3위_보궐선거");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "1-3위_ani_기초단체장(5760동일)",
            "1-3위_ani_기초단체장",
            "1-3위_기초단체장_5760",
            "1-3위_기초단체장_L",
            "1-3위_기초단체장_L_1");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "경력",
            "경력_광역단체장_in",
            "경력_기초단체장_in");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "당선",
            "당선_광역단체장",
            "당선_광역단체장_HD",
            "당선_광역단체장_L",
            "당선_광역의원",
            "당선_광역의원_HD",
            "당선_광역의원_L",
            "당선_기초단체장",
            "당선_기초단체장_HD",
            "당선_기초단체장_L",
            "당선_기초의원",
            "당선_기초의원_HD",
            "당선_기초의원_L");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "당선_교육감",
            "당선_교육감",
            "당선_교육감_HD",
            "당선_교육감_L");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "모든후보",
            "모든후보_광역단체장",
            "모든후보_광역단체장_5760",
            "모든후보_광역단체장_5760_END",
            "모든후보_광역단체장_END",
            "모든후보_광역단체장_L",
            "모든후보_광역단체장_L_END",
            "모든후보_기초단체장",
            "모든후보_기초단체장_5760",
            "모든후보_기초단체장_5760_END",
            "모든후보_기초단체장_END",
            "모든후보_기초단체장_L",
            "모든후보_기초단체장_L_END");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "모든후보_교육감",
            "모든후보_교육감",
            "모든후보_교육감_5760",
            "모든후보_교육감_5760_END",
            "모든후보_교육감_END",
            "모든후보_교육감_L",
            "모든후보_교육감_L_END");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "사전_역대당선",
            "사전_역대당선자",
            "사전_역대당선자_기초단체장");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "사전_역대당선_교육감",
            "사전_역대당선자_교육감");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "이시각1위_광역단체장",
            "이시각1위_광역단체장",
            "이시각1위_광역단체장_HD");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "이시각1위_광역단체장_5760",
            "이시각1위_광역단체장_5760",
            "이시각1위_광역단체장_L");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "이시각1위_기초단체장(5760동일)",
            "이시각1위_기초단체장",
            "이시각1위_기초단체장_HD",
            "이시각1위_기초단체장_L");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "접전,초접전",
            "접전_광역단체장",
            "접전_기초단체장",
            "초접전_광역단체장",
            "초접전_기초단체장");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            "판세_광역단체장",
            "판세_광역단체장",
            "판세_기초단체장",
            "역대시도판세_광역단체장",
            "역대시도판세_기초단체장",
            "판세_기초단체장_5760",
            "판세_기초단체장_7680");
        Add(
            mappings,
            "Elect2026_Normal_민방",
            string.Empty,
            "사전_역대투표율");

        Add(
            mappings,
            "Elect2026_Bottom_민방",
            "1-2위, 1-3위, 이시각1위",
            "1-2위_광역단체장",
            "1-2위_기초단체장",
            "1-3위_광역단체장",
            "1-3위_기초단체장",
            "1위_광역단체장",
            "1위_기초단체장");
        Add(
            mappings,
            "Elect2026_Bottom_민방",
            "당선",
            "당선_광역단체장",
            "당선_광역의원",
            "당선_기초단체장",
            "당선_기초의원");
        Add(
            mappings,
            "Elect2026_Bottom_민방",
            "모든후보",
            "전후보_광역단체장",
            "전후보_기초단체장");
        Add(
            mappings,
            "Elect2026_Bottom_민방",
            "모든후보_교육감",
            "전후보_교육감");

        Add(
            mappings,
            "Elect2026_Top_민방",
            "1-2위_사진",
            "광역단체장_2인",
            "기초단체장_2인");
        Add(
            mappings,
            "Elect2026_Top_민방",
            "1-2위_텍스트",
            "광역단체장_2인_텍스트",
            "기초단체장_2인_텍스트");
        return mappings;
    }
}

internal enum PartyColorAssetUsage
{
    Bar,
    Plate,
    Outline,
    Color,
    Group,
    Symbol
}

internal readonly record struct PartyStyleColorSpec(
    eKStyleType StyleType,
    int Order,
    byte R,
    byte G,
    byte B,
    byte A);
