using System.IO;
using System.Text;
using AxCopilot.Services;

namespace AxCopilot.Services.Agent;

/// <summary>
/// 마크다운 기반 스킬 정의를 로드/관리하는 서비스.
/// *.skill.md 파일의 YAML 프론트매터를 파싱하여 슬래시 명령으로 노출합니다.
/// 외부 폴더(%APPDATA%\AxCopilot\skills\) 또는 앱 기본 폴더에서 로드합니다.
/// </summary>
public static partial class SkillService
{
    private static List<SkillDefinition> _skills = new();
    private static string _lastFolder = "";

    /// <summary>로드된 스킬 목록.</summary>
    public static IReadOnlyList<SkillDefinition> Skills => _skills;

    /// <summary>스킬 폴더에서 *.skill.md 파일을 로드합니다.</summary>
    public static void LoadSkills(string? customFolder = null)
    {
        var folders = new List<string>();

        // 1) 앱 기본 스킬 폴더
        var defaultFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "skills");
        if (Directory.Exists(defaultFolder)) folders.Add(defaultFolder);

        // 2) 사용자 스킬 폴더 (%APPDATA%\AxCopilot\skills\)
        var appDataFolder = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
            "AxCopilot", "skills");
        if (Directory.Exists(appDataFolder)) folders.Add(appDataFolder);

        // 3) 사용자 지정 폴더
        if (!string.IsNullOrEmpty(customFolder) && Directory.Exists(customFolder))
            folders.Add(customFolder);

        var allSkills = new List<SkillDefinition>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var folder in folders)
        {
            // 1) 기존 형식: *.skill.md 파일
            foreach (var file in Directory.GetFiles(folder, "*.skill.md"))
            {
                try
                {
                    var skill = ParseSkillFile(file);
                    if (skill != null && seen.Add(skill.Name))
                        allSkills.Add(skill);
                }
                catch (Exception ex)
                {
                    LogService.Warn($"스킬 로드 실패 [{file}]: {ex.Message}");
                }
            }

            // 2) SKILL.md 표준: 하위폴더/SKILL.md 구조 (재귀 + 콜론 네임스페이싱)
            // Phase 24: database/migrate/SKILL.md → "database:migrate"
            try
            {
                LoadSkillsRecursive(folder, folder, allSkills, seen);
            }
            catch (Exception) { /* 폴더 접근 오류 무시 */ }
        }

        // 런타임 의존성 검증
        foreach (var skill in allSkills)
        {
            if (!string.IsNullOrEmpty(skill.Requires))
            {
                var runtimes = skill.Requires.Split(',').Select(r => r.Trim());
                skill.IsAvailable = runtimes.All(r => RuntimeDetector.IsAvailable(r));
            }
        }

        _skills = allSkills;
        _lastFolder = customFolder ?? "";
        var unavailCount = allSkills.Count(s => !s.IsAvailable);
        LogService.Info($"스킬 {allSkills.Count}개 로드 완료" +
            (unavailCount > 0 ? $" (런타임 미충족 {unavailCount}개)" : ""));
    }

    /// <summary>스킬 이름으로 검색합니다.</summary>
    public static SkillDefinition? Find(string name) =>
        _skills.FirstOrDefault(s => s.Name.Equals(name, StringComparison.OrdinalIgnoreCase));

    /// <summary>
    /// Phase 19-D-EXT: 스킬 본문에 인수 치환 + 인라인 명령 처리를 적용합니다.
    /// 호출 시점: 스킬이 로드된 후, LLM 프롬프트로 전달하기 직전.
    /// </summary>
    /// <param name="skill">실행할 스킬.</param>
    /// <param name="arguments">사용자가 전달한 인수 (예: /skill arg1 arg2).</param>
    /// <param name="workFolder">셸 명령 실행 작업 디렉토리.</param>
    /// <param name="ct">취소 토큰.</param>
    /// <returns>인수 치환 + 인라인 명령이 처리된 프롬프트.</returns>
    public static async Task<string> PrepareSkillBodyAsync(
        SkillDefinition skill,
        string arguments,
        string workFolder,
        CancellationToken ct)
    {
        var body = skill.SystemPrompt;

        // 1) 인수 치환 ($ARGUMENTS, $name, {0})
        var namedArgs = skill.Arguments.Count > 0 ? skill.Arguments : null;
        body = SkillArgumentSubstitution.Substitute(body, namedArgs, arguments);

        // 2) 인라인 셸 커맨드 (!`command`)
        body = await SkillInlineCommandProcessor.ProcessAsync(body, workFolder, ct);

        return body;
    }

    /// <summary>슬래시 명령어 매칭용: /로 시작하는 텍스트에 매칭되는 스킬 목록.</summary>
    public static List<SkillDefinition> MatchSlashCommand(string input)
    {
        if (!input.StartsWith('/')) return new();
        return _skills
            .Where(s => s.UserInvocable) // Phase 24: user-invocable: false 스킬 제외
            .Where(s => ("/" + s.Name).StartsWith(input, StringComparison.OrdinalIgnoreCase))
            .ToList();
    }

    /// <summary>스킬 폴더가 없으면 생성하고 예제 스킬을 배치합니다.</summary>
    public static void EnsureSkillFolder()
    {
        var folder = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
            "AxCopilot", "skills");
        if (!Directory.Exists(folder))
            Directory.CreateDirectory(folder);

        // 예제 스킬이 없으면 생성
        CreateExampleSkill(folder, "daily-standup.skill.md",
            "daily-standup", "데일리 스탠드업",
            "작업 폴더의 최근 변경사항을 요약하여 데일리 스탠드업 보고서를 생성합니다.",
            """
            작업 폴더의 Git 상태와 최근 커밋을 분석하여 데일리 스탠드업 보고서를 작성하세요.

            다음 도구를 사용하세요:
            1. git_tool (action: log, args: "--oneline -10") — 최근 커밋 확인
            2. git_tool (action: status) — 현재 변경사항 확인
            3. git_tool (action: diff, args: "--stat") — 변경 파일 통계

            보고서 형식:
            ## 📋 데일리 스탠드업 보고서

            ### ✅ 완료한 작업
            - 최근 커밋 기반으로 정리

            ### 🔄 진행 중인 작업
            - 현재 수정 중인 파일 기반

            ### ⚠️ 블로커/이슈
            - TODO/FIXME가 있으면 표시

            한국어로 작성하세요.
            """);

        CreateExampleSkill(folder, "bug-hunt.skill.md",
            "bug-hunt", "버그 탐색",
            "작업 폴더에서 잠재적 버그 패턴을 검색합니다.",
            """
            작업 폴더의 코드에서 잠재적 버그 패턴을 찾아 보고하세요.

            다음 도구를 사용하세요:
            1. grep_tool — 위험 패턴 검색:
               - 빈 catch 블록: catch\s*\{\s*\}
               - TODO/FIXME: (TODO|FIXME|HACK|XXX)
               - .Result/.Wait(): \.(Result|Wait\(\))
               - 하드코딩된 자격증명: (password|secret|apikey)\s*=\s*"
            2. code_review (action: diff_review, focus: bugs) — 최근 변경사항 버그 검사

            결과를 심각도별로 분류하여 보고하세요:
            - 🔴 CRITICAL: 즉시 수정 필요
            - 🟡 WARNING: 검토 필요
            - 🔵 INFO: 개선 권장

            한국어로 작성하세요.
            """);

        CreateExampleSkill(folder, "code-explain.skill.md",
            "code-explain", "코드 설명",
            "지정한 파일의 코드를 상세히 설명합니다.",
            """
            사용자가 지정한 파일 또는 작업 폴더의 주요 파일을 읽고 상세히 설명하세요.

            다음 도구를 사용하세요:
            1. file_read — 파일 내용 읽기
            2. folder_map — 프로젝트 구조 파악 (필요시)

            설명 포함 사항:
            - 파일의 역할과 책임
            - 주요 클래스/함수의 목적
            - 데이터 흐름
            - 외부 의존성
            - 개선 포인트 (있다면)

            한국어로 쉽게 설명하세요. 코드 블록을 활용하여 핵심 부분을 인용하세요.
            """);
    }

    // ─── 내부 메서드 ─────────────────────────────────────────────────────────

    /// <summary>
    /// Phase 24: 하위 디렉토리를 재귀 탐색하여 SKILL.md를 찾고,
    /// 경로를 콜론(:)으로 구분된 네임스페이스 이름으로 변환합니다.
    /// 예: skills/database/migrate/SKILL.md → "database:migrate"
    /// </summary>
    private static void LoadSkillsRecursive(
        string rootFolder,
        string currentFolder,
        List<SkillDefinition> allSkills,
        HashSet<string> seen)
    {
        foreach (var subDir in Directory.GetDirectories(currentFolder))
        {
            var skillMd = Path.Combine(subDir, "SKILL.md");
            if (File.Exists(skillMd))
            {
                try
                {
                    // 네임스페이스: rootFolder로부터의 상대경로를 콜론으로 치환
                    var relativePath = Path.GetRelativePath(rootFolder, subDir).Replace('\\', '/');
                    var namespacedName = relativePath.Replace('/', ':');

                    var skill = ParseSkillFile(skillMd, namespacedName);
                    if (skill != null && seen.Add(skill.Name))
                        allSkills.Add(skill);
                }
                catch (Exception ex)
                {
                    LogService.Warn($"스킬 로드 ��패 [{skillMd}]: {ex.Message}");
                }
            }
            else
            {
                // SKILL.md가 없는 중간 디렉토리 → 더 깊이 ���색
                LoadSkillsRecursive(rootFolder, subDir, allSkills, seen);
            }
        }
    }

    private static void CreateExampleSkill(string folder, string fileName, string name, string label, string description, string body)
    {
        var path = Path.Combine(folder, fileName);
        if (File.Exists(path)) return;

        var content = $"""
            ---
            name: {name}
            label: {label}
            description: {description}
            icon: \uE768
            ---

            {body.Trim()}
            """;
        // 들여쓰기 정리 (raw string literal의 인덴트 제거)
        var lines = content.Split('\n').Select(l => l.TrimStart()).ToArray();
        File.WriteAllText(path, string.Join('\n', lines), Encoding.UTF8);
    }

    /// <summary>*.skill.md 파일을 파싱합니다.</summary>
    private static SkillDefinition? ParseSkillFile(string filePath) => ParseSkillFile(filePath, null);

    /// <summary>*.skill.md 파일을 파싱합니다. namespacedName이 주어지면 이름 오버라이드.</summary>
    private static SkillDefinition? ParseSkillFile(string filePath, string? namespacedName)
    {
        var content = File.ReadAllText(filePath, Encoding.UTF8);
        if (!content.TrimStart().StartsWith("---"))
            return null;

        // 프론트매터 추출
        var firstSep = content.IndexOf("---", StringComparison.Ordinal);
        var secondSep = content.IndexOf("---", firstSep + 3, StringComparison.Ordinal);
        if (secondSep < 0) return null;

        var frontmatter = content[(firstSep + 3)..secondSep].Trim();
        var body = content[(secondSep + 3)..].Trim();

        // 키-값 파싱 (YAML 1단계 + metadata 맵 지원)
        var meta = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        string? currentMap = null;
        foreach (var line in frontmatter.Split('\n'))
        {
            // 들여쓰기된 줄 → 현재 맵의 하위 키
            if (currentMap != null && (line.StartsWith("  ") || line.StartsWith("\t")))
            {
                var trimmed = line.TrimStart();
                var colonIdx = trimmed.IndexOf(':');
                if (colonIdx > 0)
                {
                    var subKey = trimmed[..colonIdx].Trim();
                    var subVal = trimmed[(colonIdx + 1)..].Trim().Trim('"', '\'');
                    meta[$"{currentMap}.{subKey}"] = subVal;
                }
                continue;
            }
            currentMap = null;

            var ci = line.IndexOf(':');
            if (ci > 0)
            {
                var key = line[..ci].Trim();
                var value = line[(ci + 1)..].Trim().Trim('"', '\'');
                if (string.IsNullOrEmpty(value))
                {
                    // 빈 값 = 맵 시작 (metadata:)
                    currentMap = key;
                }
                else
                {
                    meta[key] = value;
                }
            }
        }

        // 폴더명을 기본 이름으로 사용 (SKILL.md 표준: 폴더명 = name)
        var dirName = Path.GetFileName(Path.GetDirectoryName(filePath) ?? "");
        var fileName = Path.GetFileNameWithoutExtension(filePath).Replace(".skill", "");
        var fallbackName = filePath.EndsWith("SKILL.md", StringComparison.OrdinalIgnoreCase) ? dirName : fileName;

        // Phase 24: 네임스페이스 이름 우선, 그 다음 프론트매터, 그 다음 폴더명
        var name = namespacedName ?? meta.GetValueOrDefault("name", fallbackName);
        if (string.IsNullOrEmpty(name)) return null;

        // SKILL.md 표준: label/icon은 metadata 맵에 있을 수 있음
        var label = meta.GetValueOrDefault("label", "") ?? "";
        var icon = meta.GetValueOrDefault("icon", "") ?? "";

        // metadata.label / metadata.icon 지원 (SKILL.md 표준)
        if (string.IsNullOrEmpty(label) && meta.TryGetValue("metadata.label", out var ml))
            label = ml ?? "";
        if (string.IsNullOrEmpty(icon) && meta.TryGetValue("metadata.icon", out var mi))
            icon = mi ?? "";

        // Phase 24: arguments 필드 파싱 (YAML 리스트 또는 쉼표 구분)
        var argsRaw = meta.GetValueOrDefault("arguments", "") ?? "";
        var arguments = string.IsNullOrWhiteSpace(argsRaw)
            ? Array.Empty<string>()
            : argsRaw.Trim('[', ']').Split(',').Select(a => a.Trim().Trim('"', '\'')).Where(a => a.Length > 0).ToArray();

        // user-invocable: 기본 true, "false"면 false
        var userInvocable = !string.Equals(
            meta.GetValueOrDefault("user-invocable", "true"), "false", StringComparison.OrdinalIgnoreCase);

        return new SkillDefinition
        {
            Id = name,
            Name = name,
            Label = string.IsNullOrEmpty(label) ? name : label,
            Description = meta.GetValueOrDefault("description", "") ?? "",
            Icon = string.IsNullOrEmpty(icon) ? "\uE768" : ConvertUnicodeEscape(icon),
            SystemPrompt = MapToolNames(body),
            FilePath = filePath,
            License = meta.GetValueOrDefault("license", "") ?? "",
            Compatibility = meta.GetValueOrDefault("compatibility", "") ?? "",
            AllowedTools = meta.GetValueOrDefault("allowed-tools", "") ?? "",
            Requires = meta.GetValueOrDefault("requires", "") ?? "",
            Tabs = meta.GetValueOrDefault("tabs", "all") ?? "all",
            // Phase 24: 고급 프론트매터 필드
            Context = meta.GetValueOrDefault("context", "") ?? "",
            ModelOverride = meta.GetValueOrDefault("model", "") ?? "",
            UserInvocable = userInvocable,
            Paths = meta.GetValueOrDefault("paths", "") ?? "",
            ScopedHooks = meta.GetValueOrDefault("hooks", "") ?? "",
            Arguments = arguments,
            WhenToUse = meta.GetValueOrDefault("when_to_use",
                meta.GetValueOrDefault("when-to-use", "") ?? "") ?? "",
            Version = meta.GetValueOrDefault("version", "") ?? "",
        };
    }

    /// <summary>YAML의 \uXXXX 이스케이프를 실제 유니코드 문자로 변환합니다.</summary>
    private static string ConvertUnicodeEscape(string value)
    {
        if (string.IsNullOrEmpty(value)) return value;
        return System.Text.RegularExpressions.Regex.Replace(
            value, @"\\u([0-9a-fA-F]{4})",
            m => ((char)Convert.ToInt32(m.Groups[1].Value, 16)).ToString());
    }
}

