using AxCopilot.Services;

namespace AxCopilot.Core;

/// <summary>
/// 부분 입력·오타도 유사 항목을 찾아주는 Fuzzy 검색 엔진.
/// 한글 초성 검색, 자모 분리 검색, 비연속 매칭을 지원합니다.
/// </summary>
public class FuzzyEngine
{
    private readonly IndexService _index;

    public FuzzyEngine(IndexService index)
    {
        _index = index;
    }

    /// <summary>
    /// 필터 조건을 포함한 검색. 텍스트 쿼리가 없으면 모든 항목에서 필터 적용.
    /// </summary>
    public IEnumerable<FuzzyResult> SearchWithFilter(
        string query, Func<IndexEntry, bool> filter, int maxResults = 7)
    {
        if (string.IsNullOrWhiteSpace(query))
        {
            // 텍스트 없이 필터만 있는 경우: 전체 항목에서 조건 검색
            return _index.Entries
                .AsParallel()
                .Where(filter)
                .OrderByDescending(e => e.Score)
                .Take(maxResults)
                .Select(e => new FuzzyResult(e, e.Score + 100));
        }

        // 텍스트 + 필터: 퍼지 후보 확장(×15) → 필터 적용
        return Search(query, maxResults * 15)
            .Where(r => filter(r.Entry))
            .Take(maxResults);
    }

    /// <summary>
    /// 쿼리에 대해 유사도 순으로 정렬된 결과를 반환합니다.
    /// 300개 이상 항목은 PLINQ 병렬 처리로 검색 속도를 개선합니다.
    /// </summary>
    public IEnumerable<FuzzyResult> Search(string query, int maxResults = 7)
    {
        if (string.IsNullOrWhiteSpace(query))
            return Enumerable.Empty<FuzzyResult>();

        var normalized = query.Trim().ToLowerInvariant();
        var entries = _index.Entries;

        // 쿼리 언어 타입 1회 사전 분류 — 항목마다 재계산하지 않음
        bool queryHasKorean = false;
        foreach (var c in normalized)
        {
            if ((c >= 0xAC00 && c <= 0xD7A3) || ChosungSet.Contains(c))
            { queryHasKorean = true; break; }
        }

        // 300개 초과 시 PLINQ 병렬 처리
        if (entries.Count > 300)
        {
            return entries.AsParallel()
                .Select(e => new FuzzyResult(e, CalculateScoreFast(normalized, e, queryHasKorean)))
                .Where(r => r.Score > 0)
                .OrderByDescending(r => r.Score)
                .Take(maxResults);
        }

        return entries
            .Select(e => new FuzzyResult(e, CalculateScoreFast(normalized, e, queryHasKorean)))
            .Where(r => r.Score > 0)
            .OrderByDescending(r => r.Score)
            .Take(maxResults);
    }

    /// <summary>미리 계산된 캐시 필드를 활용하는 빠른 점수 계산.</summary>
    private static int CalculateScoreFast(string query, IndexEntry entry, bool queryHasKorean)
    {
        // 캐시가 없으면(구버전 호환) 기존 방식으로 폴백
        var targetLower = string.IsNullOrEmpty(entry.NameLower)
            ? entry.Name.ToLowerInvariant()
            : entry.NameLower;

        if (query.Length == 0) return 0;

        if (targetLower == query) return 1000 + entry.Score;
        if (targetLower.StartsWith(query)) return 800 + entry.Score;
        if (targetLower.Contains(query)) return 600 + entry.Score;

        // 순수 ASCII 쿼리 — 한글 검색 로직(자모·초성) 전체 스킵
        if (!queryHasKorean)
        {
            var fs = FuzzyMatch(query, targetLower);
            return fs > 0 ? fs + entry.Score : 0;
        }

        // 자모 분리 검색 (캐시된 NameJamo 활용)
        var jamoScore = JamoContainsScoreFast(
            string.IsNullOrEmpty(entry.NameJamo) ? DecomposeToJamo(targetLower) : entry.NameJamo,
            query);
        if (jamoScore > 0) return jamoScore + entry.Score;

        // 초성 검색 (캐시된 NameChosung 활용)
        var chosungScore = ChosungMatchScoreFast(
            string.IsNullOrEmpty(entry.NameChosung) ? null : entry.NameChosung,
            targetLower, query);
        if (chosungScore > 0) return chosungScore + entry.Score;

        // Fuzzy 매칭
        var fuzzyScore = FuzzyMatch(query, targetLower);
        if (fuzzyScore > 0) return fuzzyScore + entry.Score;

        return 0;
    }

    /// <summary>
    /// 점수 계산: 정확 일치 > 시작 일치 > 포함 일치 > 자모 포함 > 초성 > Fuzzy
    /// </summary>
    internal static int CalculateScore(string query, string target, int baseScore)
    {
        if (query.Length == 0) return 0;

        if (target == query) return 1000 + baseScore;          // 완전 일치
        if (target.StartsWith(query)) return 800 + baseScore;  // 시작 일치
        if (target.Contains(query)) return 600 + baseScore;    // 부분 일치

        // 한글 자모 분리 후 부분 일치 ("모장" → 메모장)
        var jamoScore = JamoContainsScore(target, query);
        if (jamoScore > 0) return jamoScore + baseScore;

        // 한글 초성 검색 (순수 초성 + 혼합 쿼리 모두 지원)
        var chosungScore = ChosungMatchScore(target, query);
        if (chosungScore > 0) return chosungScore + baseScore;

        // 문자 순서 포함 (Fuzzy)
        var fuzzyScore = FuzzyMatch(query, target);
        if (fuzzyScore > 0) return fuzzyScore + baseScore;

        return 0;
    }

    // ─── Fuzzy Match ────────────────────────────────────────────────────────

    /// <summary>
    /// 쿼리의 모든 문자가 target에 순서대로 포함되는지 확인 (subsequence)
    /// </summary>
    internal static int FuzzyMatch(string query, string target)
    {
        int qi = 0, ti = 0;
        int score = 0;
        int lastMatchIdx = -1;

        while (qi < query.Length && ti < target.Length)
        {
            if (query[qi] == target[ti])
            {
                if (lastMatchIdx == ti - 1) score += 30;  // 연속 매칭 보너스
                else score += 10;                          // 비연속 매칭

                if (ti == 0) score += 15;  // 시작 위치 보너스

                lastMatchIdx = ti;
                qi++;
            }
            ti++;
        }

        return qi == query.Length ? Math.Max(score, 50) : 0;  // 최소 50점 보장
    }

    // ─── 한글 자모 분리 ─────────────────────────────────────────────────────

    private static readonly char[] Chosungs =
        ['ㄱ','ㄲ','ㄴ','ㄷ','ㄸ','ㄹ','ㅁ','ㅂ','ㅃ','ㅅ','ㅆ','ㅇ','ㅈ','ㅉ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ'];

    /// <summary>초성 O(1) 조회용 HashSet — HasChosung/IsChosung/MixedMatch에서 사용.</summary>
    private static readonly HashSet<char> ChosungSet =
        new(['ㄱ','ㄲ','ㄴ','ㄷ','ㄸ','ㄹ','ㅁ','ㅂ','ㅃ','ㅅ','ㅆ','ㅇ','ㅈ','ㅉ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ']);

    private static readonly char[] Jungsungs =
        ['ㅏ','ㅐ','ㅑ','ㅒ','ㅓ','ㅔ','ㅕ','ㅖ','ㅗ','ㅘ','ㅙ','ㅚ','ㅛ','ㅜ','ㅝ','ㅞ','ㅟ','ㅠ','ㅡ','ㅢ','ㅣ'];

    private static readonly char[] Jongsungs =
        ['\0','ㄱ','ㄲ','ㄳ','ㄴ','ㄵ','ㄶ','ㄷ','ㄹ','ㄺ','ㄻ','ㄼ','ㄽ','ㄾ','ㄿ','ㅀ','ㅁ','ㅂ','ㅄ','ㅅ','ㅆ','ㅇ','ㅈ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ'];

    /// <summary>한글 음절을 자모로 분리 (초성+중성+종성). 비한글은 그대로 반환.</summary>
    internal static string DecomposeToJamo(string text)
    {
        var result = new System.Text.StringBuilder(text.Length * 3);
        foreach (var c in text)
        {
            if (c >= 0xAC00 && c <= 0xD7A3)
            {
                int offset = c - 0xAC00;
                int cho = offset / (21 * 28);
                int jung = (offset % (21 * 28)) / 28;
                int jong = offset % 28;

                result.Append(Chosungs[cho]);
                result.Append(Jungsungs[jung]);
                if (jong > 0) result.Append(Jongsungs[jong]);
            }
            else
            {
                result.Append(c);
            }
        }
        return result.ToString();
    }

    /// <summary>한글 음절에서 초성만 추출. 비한글은 '\0'.</summary>
    internal static char GetChosung(char hangul)
    {
        if (hangul < 0xAC00 || hangul > 0xD7A3) return '\0';
        int offset = hangul - 0xAC00;
        return Chosungs[offset / (21 * 28)];
    }

    // ─── 자모 기반 포함 검색 ────────────────────────────────────────────────

    /// <summary>
    /// 쿼리를 자모 분리 후 target의 자모에 연속 부분 문자열로 포함되는지 확인.
    /// "모장" → "ㅁㅗㅈㅏㅇ" 이 "ㅁㅔㅁㅗㅈㅏㅇ"(메모장)에 포함 → 550점
    /// </summary>
    internal static int JamoContainsScore(string target, string query)
    {
        if (!HasKorean(query)) return 0;

        var targetJamo = DecomposeToJamo(target);
        var queryJamo = DecomposeToJamo(query);

        if (queryJamo.Length == 0 || targetJamo.Length == 0) return 0;

        // 자모 연속 포함
        if (targetJamo.Contains(queryJamo))
        {
            // 시작 위치가 음절 경계(초성)에 가까울수록 높은 점수
            int idx = targetJamo.IndexOf(queryJamo);
            return idx == 0 ? 580 : 550;
        }

        // 자모 subsequence 매칭 (비연속이지만 순서 유지)
        int qi = 0;
        for (int ti = 0; ti < targetJamo.Length && qi < queryJamo.Length; ti++)
        {
            if (queryJamo[qi] == targetJamo[ti]) qi++;
        }
        if (qi == queryJamo.Length) return 400;

        return 0;
    }

    // ─── 초성 검색 ──────────────────────────────────────────────────────────

    /// <summary>쿼리에 초성 문자가 하나라도 포함되어 있는지.</summary>
    internal static bool HasChosung(string text) => text.Any(c => ChosungSet.Contains(c));

    /// <summary>문자열의 모든 문자가 초성인지.</summary>
    internal static bool IsChosung(string text) => text.Length > 0 && text.All(c => ChosungSet.Contains(c));

    /// <summary>문자열에 한글(완성형)이 포함되어 있는지.</summary>
    private static bool HasKorean(string text) => text.Any(c => c >= 0xAC00 && c <= 0xD7A3);

    /// <summary>
    /// 초성 매칭 점수. 순수 초성 쿼리 + 혼합 쿼리(초성+완성형) 모두 지원.
    /// 비연속 매칭도 허용 ("ㅁㅊ" → 메모장 OK).
    /// </summary>
    internal static int ChosungMatchScore(string target, string query)
    {
        // 초성이 하나도 없으면 초성 검색 아님
        if (!HasChosung(query)) return 0;

        // 타겟에서 각 글자의 초성 추출
        var targetChosungs = new List<char>();
        var targetChars = new List<char>();
        foreach (var c in target)
        {
            var cho = GetChosung(c);
            if (cho != '\0')
            {
                targetChosungs.Add(cho);
                targetChars.Add(c);
            }
            else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
            {
                targetChosungs.Add(c);
                targetChars.Add(c);
            }
        }

        if (targetChosungs.Count == 0) return 0;

        // 순수 초성 쿼리 ("ㅁㅁㅈ", "ㅁㅊ")
        if (IsChosung(query))
        {
            // 연속 매칭 시도
            if (ContainsChosungConsecutive(targetChosungs, query)) return 520;
            // 비연속(subsequence) 매칭
            if (ContainsChosungSubsequence(targetChosungs, query)) return 480;
            return 0;
        }

        // 혼합 쿼리 ("ㅁ장", "계ㅅ기") — 초성+완성형 혼합
        return MixedChosungMatch(targetChars, targetChosungs, query);
    }

    /// <summary>연속 초성 매칭 (기존 로직 유지)</summary>
    private static bool ContainsChosungConsecutive(List<char> targetChosungs, string query)
    {
        for (int i = 0; i <= targetChosungs.Count - query.Length; i++)
        {
            bool match = true;
            for (int j = 0; j < query.Length; j++)
            {
                if (targetChosungs[i + j] != query[j]) { match = false; break; }
            }
            if (match) return true;
        }
        return false;
    }

    /// <summary>비연속 초성 매칭 (subsequence)</summary>
    private static bool ContainsChosungSubsequence(List<char> targetChosungs, string query)
    {
        int qi = 0;
        for (int ti = 0; ti < targetChosungs.Count && qi < query.Length; ti++)
        {
            if (targetChosungs[ti] == query[qi]) qi++;
        }
        return qi == query.Length;
    }

    /// <summary>
    /// 혼합 쿼리 매칭: 초성은 초성끼리, 완성형은 완성형끼리 비교.
    /// "ㅁ장" → target[i]의 초성이 'ㅁ'이고, target[j>i]가 '장'인지.
    /// </summary>
    private static int MixedChosungMatch(List<char> targetChars, List<char> targetChosungs, string query)
    {
        int qi = 0, ti = 0;
        while (qi < query.Length && ti < targetChars.Count)
        {
            var qc = query[qi];
            if (ChosungSet.Contains(qc))
            {
                // 쿼리 문자가 초성 → 타겟 초성과 비교
                if (targetChosungs[ti] == qc) qi++;
            }
            else
            {
                // 쿼리 문자가 완성형 → 타겟 원본 문자와 비교
                if (targetChars[ti] == qc) qi++;
            }
            ti++;
        }
        return qi == query.Length ? 460 : 0;
    }

    // ─── 캐시 기반 빠른 검색 메서드 ────────────────────────────────────────────

    /// <summary>미리 계산된 자모 문자열을 사용하는 빠른 자모 포함 검색.</summary>
    private static int JamoContainsScoreFast(string targetJamo, string query)
    {
        if (!HasKorean(query)) return 0;

        var queryJamo = DecomposeToJamo(query); // 쿼리는 짧으므로 매번 분해해도 빠름
        if (queryJamo.Length == 0 || targetJamo.Length == 0) return 0;

        if (targetJamo.Contains(queryJamo))
        {
            int idx = targetJamo.IndexOf(queryJamo, StringComparison.Ordinal);
            return idx == 0 ? 580 : 550;
        }

        int qi = 0;
        for (int ti = 0; ti < targetJamo.Length && qi < queryJamo.Length; ti++)
        {
            if (queryJamo[qi] == targetJamo[ti]) qi++;
        }
        return qi == queryJamo.Length ? 400 : 0;
    }

    /// <summary>미리 계산된 초성 문자열을 사용하는 빠른 초성 검색.</summary>
    private static int ChosungMatchScoreFast(string? targetChosung, string targetLower, string query)
    {
        if (!HasChosung(query)) return 0;

        if (IsChosung(query))
        {
            if (string.IsNullOrEmpty(targetChosung)) return 0;
            // 연속 매칭: 단순 Contains
            if (targetChosung.Contains(query, StringComparison.Ordinal)) return 520;
            // 비연속 매칭 (subsequence)
            int qi2 = 0;
            for (int ti2 = 0; ti2 < targetChosung.Length && qi2 < query.Length; ti2++)
            {
                if (targetChosung[ti2] == query[qi2]) qi2++;
            }
            if (qi2 == query.Length) return 480;
            return 0;
        }

        // 혼합 쿼리(초성+완성형): List<char> 할당 없는 인라인 매칭
        {
            int qi2 = 0, ti2 = 0;
            while (qi2 < query.Length && ti2 < targetLower.Length)
            {
                var qc = query[qi2];
                var tc = targetLower[ti2];
                if (ChosungSet.Contains(qc))
                {
                    // 쿼리가 초성 → 타겟 문자의 초성과 비교
                    var cho = GetChosung(tc);
                    if (cho == '\0' && ((tc >= 'a' && tc <= 'z') || (tc >= '0' && tc <= '9')))
                        cho = tc; // 영문/숫자는 초성 = 자신
                    if (cho == qc) qi2++;
                }
                else
                {
                    // 쿼리가 완성형 → 타겟 원본 문자와 비교
                    if (tc == qc) qi2++;
                }
                ti2++;
            }
            return qi2 == query.Length ? 460 : 0;
        }
    }

    // ─── 하위 호환 ──────────────────────────────────────────────────────────

    /// <summary>기존 API 호환용 — ContainsChosung(연속+비연속)</summary>
    internal static bool ContainsChosung(string target, string chosungQuery)
    {
        var targetChosungs = target.Select(GetChosung).Where(c => c != '\0').ToList();
        if (targetChosungs.Count < chosungQuery.Length) return false;

        return ContainsChosungConsecutive(targetChosungs, chosungQuery)
            || ContainsChosungSubsequence(targetChosungs, chosungQuery);
    }
}

public record FuzzyResult(IndexEntry Entry, int Score);
