using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Data.Sqlite;

namespace AxCopilot.Services;

/// <summary>
/// 프로젝트 코드베이스 인덱싱 및 시맨틱 검색 서비스.
/// TF-IDF 기반 유사도 검색을 SQLite에 영속 저장하여
/// 증분 업데이트와 빠른 재시작을 지원합니다.
/// (로컬 전용, 외부 서버 불필요)
/// </summary>
public partial class CodeIndexService : IDisposable
{
    private static App? CurrentApp => System.Windows.Application.Current as App;

    private SqliteConnection? _db;
    private string _workFolder = "";
    private bool _indexed;
    private int _totalDocs;

    public bool IsIndexed => _indexed;
    public int ChunkCount => _totalDocs;

    // ── 스톱워드 (TF-IDF 정확도 향상) ──────────────────────────────────
    private static readonly HashSet<string> StopWords = new(StringComparer.OrdinalIgnoreCase)
    {
        // 영어 공통
        "the", "is", "at", "of", "on", "and", "or", "not", "in", "to", "for",
        "it", "be", "as", "do", "by", "this", "that", "with", "from", "but",
        "an", "are", "was", "were", "been", "being", "have", "has", "had",
        "if", "else", "then", "than", "so", "no", "yes",
        // 프로그래밍 공통 (너무 빈번해서 변별력 없음)
        "var", "int", "string", "void", "null", "new", "return", "get", "set",
        "public", "private", "class", "static", "using", "namespace", "true", "false",
        "import", "export", "function", "const", "let", "def", "self",
    };

    private static readonly HashSet<string> CodeExtensions = new(StringComparer.OrdinalIgnoreCase)
    {
        ".cs", ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".cpp", ".c", ".h", ".hpp",
        ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala",
        ".html", ".css", ".scss", ".json", ".xml", ".yaml", ".yml",
        ".md", ".txt", ".sql", ".sh", ".bat", ".ps1",
        ".csproj", ".sln", ".gradle", ".pom",
    };

    // ── DB 초기화 ───────────────────────────────────────────────────────

    private string GetDbPath(string workFolder)
    {
        // %APPDATA%\AxCopilot\index\{folderHash}.db
        var hash = Convert.ToHexString(
            System.Security.Cryptography.SHA256.HashData(
                Encoding.UTF8.GetBytes(workFolder.ToLowerInvariant())))[..16];
        var dir = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
            "AxCopilot", "index");
        Directory.CreateDirectory(dir);
        return Path.Combine(dir, $"{hash}.db");
    }

    private void EnsureDb(string workFolder)
    {
        if (_db != null && _workFolder == workFolder) return;

        _db?.Dispose();
        _workFolder = workFolder;
        var dbPath = GetDbPath(workFolder);
        _db = new SqliteConnection($"Data Source={dbPath}");
        _db.Open();

        // WAL 모드 (동시 읽기/쓰기 성능)
        using (var cmd = _db.CreateCommand())
        {
            cmd.CommandText = "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;";
            cmd.ExecuteNonQuery();
        }

        // 테이블 생성
        using var create = _db.CreateCommand();
        create.CommandText = """
            CREATE TABLE IF NOT EXISTS files (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                path TEXT NOT NULL UNIQUE,
                last_modified TEXT NOT NULL,
                file_size INTEGER NOT NULL
            );
            CREATE TABLE IF NOT EXISTS chunks (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                file_id INTEGER NOT NULL,
                start_line INTEGER NOT NULL,
                end_line INTEGER NOT NULL,
                content TEXT NOT NULL,
                FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
            );
            CREATE TABLE IF NOT EXISTS tokens (
                chunk_id INTEGER NOT NULL,
                token TEXT NOT NULL,
                tf INTEGER NOT NULL,
                FOREIGN KEY (chunk_id) REFERENCES chunks(id) ON DELETE CASCADE
            );
            CREATE TABLE IF NOT EXISTS doc_freq (
                token TEXT PRIMARY KEY,
                df INTEGER NOT NULL
            );
            CREATE TABLE IF NOT EXISTS meta (
                key TEXT PRIMARY KEY,
                value TEXT NOT NULL
            );
            CREATE INDEX IF NOT EXISTS idx_tokens_chunk ON tokens(chunk_id);
            CREATE INDEX IF NOT EXISTS idx_tokens_token ON tokens(token);
            CREATE INDEX IF NOT EXISTS idx_chunks_file ON chunks(file_id);
        """;
        create.ExecuteNonQuery();
    }

    // ── 인덱싱 ──────────────────────────────────────────────────────────

    /// <summary>작업 폴더의 코드 파일을 인덱싱합니다. 증분 업데이트 지원.</summary>
    public async Task IndexAsync(string workFolder, CancellationToken ct = default)
    {
        if (string.IsNullOrEmpty(workFolder) || !Directory.Exists(workFolder))
            return;

        EnsureDb(workFolder);

        await Task.Run(() =>
        {
            var existingFiles = LoadExistingFiles();
            // 설정에서 최대 파일 크기 조회
            var maxFileKb = 500;
            try
            {
                var cfgMax = CurrentApp?.SettingsService?.Settings.Llm.Code.CodeIndexMaxFileKb ?? 500;
                if (cfgMax > 0) maxFileKb = cfgMax;
            }
            catch (Exception) { }

            var currentFiles = ScanFiles(workFolder, maxFileKb);

            int added = 0, updated = 0, removed = 0;

            // 삭제된 파일 제거
            foreach (var (path, fileId) in existingFiles)
            {
                if (!currentFiles.ContainsKey(path))
                {
                    RemoveFileFromIndex(fileId);
                    removed++;
                }
            }

            // 신규/변경 파일 인덱싱
            foreach (var (relPath, info) in currentFiles)
            {
                if (ct.IsCancellationRequested) break;

                var lastMod = info.LastWriteTimeUtc.ToString("O");
                var size = info.Length;

                if (existingFiles.TryGetValue(relPath, out var fileId))
                {
                    // 기존 파일 — 변경 여부 확인
                    if (!IsFileChanged(fileId, lastMod, size))
                        continue;

                    RemoveFileFromIndex(fileId);
                    updated++;
                }
                else
                {
                    added++;
                }

                IndexFile(workFolder, relPath, lastMod, size);
            }

            // DF 테이블 재계산
            RebuildDocFreq();

            // 총 청크 수 캐시
            _totalDocs = GetTotalChunkCount();
            _indexed = _totalDocs > 0;

            // 메타 저장
            SaveMeta("lastIndexed", DateTime.UtcNow.ToString("O"));
            SaveMeta("workFolder", workFolder);

            LogService.Info($"코드 인덱싱 완료: {_totalDocs}개 청크 (추가:{added} 갱신:{updated} 삭제:{removed}) [{workFolder}]");
        }, ct);
    }

    private Dictionary<string, int> LoadExistingFiles()
    {
        var dict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
        using var cmd = _db!.CreateCommand();
        cmd.CommandText = "SELECT id, path FROM files";
        using var reader = cmd.ExecuteReader();
        while (reader.Read())
            dict[reader.GetString(1)] = reader.GetInt32(0);
        return dict;
    }

    private Dictionary<string, FileInfo> ScanFiles(string workFolder, int maxFileKb = 500)
    {
        var dict = new Dictionary<string, FileInfo>(StringComparer.OrdinalIgnoreCase);
        var maxBytes = (long)maxFileKb * 1024;
        try
        {
            var files = Directory.EnumerateFiles(workFolder, "*.*", new EnumerationOptions
            {
                RecurseSubdirectories = true,
                IgnoreInaccessible = true,
                MaxRecursionDepth = 8,
            });

            foreach (var file in files)
            {
                var ext = Path.GetExtension(file);
                if (!CodeExtensions.Contains(ext)) continue;

                try
                {
                    var info = new FileInfo(file);
                    if (info.Length > maxBytes) continue;
                    var relPath = Path.GetRelativePath(workFolder, file);
                    dict[relPath] = info;
                }
                catch (Exception) { }
            }
        }
        catch (Exception) { }
        return dict;
    }

    private bool IsFileChanged(int fileId, string lastMod, long size)
    {
        using var cmd = _db!.CreateCommand();
        cmd.CommandText = "SELECT last_modified, file_size FROM files WHERE id = @id";
        cmd.Parameters.AddWithValue("@id", fileId);
        using var reader = cmd.ExecuteReader();
        if (!reader.Read()) return true;
        return reader.GetString(0) != lastMod || reader.GetInt64(1) != size;
    }

    private void RemoveFileFromIndex(int fileId)
    {
        // 청크 ID 목록
        var chunkIds = new List<int>();
        using (var cmd = _db!.CreateCommand())
        {
            cmd.CommandText = "SELECT id FROM chunks WHERE file_id = @fid";
            cmd.Parameters.AddWithValue("@fid", fileId);
            using var reader = cmd.ExecuteReader();
            while (reader.Read()) chunkIds.Add(reader.GetInt32(0));
        }

        // 토큰 삭제
        foreach (var cid in chunkIds)
        {
            using var cmd = _db!.CreateCommand();
            cmd.CommandText = "DELETE FROM tokens WHERE chunk_id = @cid";
            cmd.Parameters.AddWithValue("@cid", cid);
            cmd.ExecuteNonQuery();
        }

        // 청크 삭제
        using (var cmd = _db!.CreateCommand())
        {
            cmd.CommandText = "DELETE FROM chunks WHERE file_id = @fid";
            cmd.Parameters.AddWithValue("@fid", fileId);
            cmd.ExecuteNonQuery();
        }

        // 파일 삭제
        using (var cmd = _db!.CreateCommand())
        {
            cmd.CommandText = "DELETE FROM files WHERE id = @fid";
            cmd.Parameters.AddWithValue("@fid", fileId);
            cmd.ExecuteNonQuery();
        }
    }

    private void IndexFile(string workFolder, string relPath, string lastMod, long size)
    {
        try
        {
            var fullPath = Path.Combine(workFolder, relPath);
            var content = File.ReadAllText(fullPath, Encoding.UTF8);

            // 파일 등록
            int fileId;
            using (var cmd = _db!.CreateCommand())
            {
                cmd.CommandText = "INSERT INTO files (path, last_modified, file_size) VALUES (@p, @m, @s) RETURNING id";
                cmd.Parameters.AddWithValue("@p", relPath);
                cmd.Parameters.AddWithValue("@m", lastMod);
                cmd.Parameters.AddWithValue("@s", size);
                fileId = Convert.ToInt32(cmd.ExecuteScalar());
            }

            // 청크 분할 (50라인씩)
            var lines = content.Split('\n');
            using var tx = _db!.BeginTransaction();

            for (int i = 0; i < lines.Length; i += 50)
            {
                var chunkLines = lines.AsSpan(i, Math.Min(50, lines.Length - i));
                var chunkText = string.Join("\n", chunkLines.ToArray());
                if (string.IsNullOrWhiteSpace(chunkText)) continue;

                var endLine = Math.Min(i + 50, lines.Length);

                // 청크 저장
                int chunkId;
                using (var cmd = _db!.CreateCommand())
                {
                    cmd.Transaction = tx;
                    cmd.CommandText = "INSERT INTO chunks (file_id, start_line, end_line, content) VALUES (@f, @s, @e, @c) RETURNING id";
                    cmd.Parameters.AddWithValue("@f", fileId);
                    cmd.Parameters.AddWithValue("@s", i + 1);
                    cmd.Parameters.AddWithValue("@e", endLine);
                    cmd.Parameters.AddWithValue("@c", chunkText);
                    chunkId = Convert.ToInt32(cmd.ExecuteScalar());
                }

                // 토큰 저장
                var tokens = Tokenize(chunkText);
                foreach (var (token, tf) in tokens)
                {
                    using var cmd = _db!.CreateCommand();
                    cmd.Transaction = tx;
                    cmd.CommandText = "INSERT INTO tokens (chunk_id, token, tf) VALUES (@cid, @t, @tf)";
                    cmd.Parameters.AddWithValue("@cid", chunkId);
                    cmd.Parameters.AddWithValue("@t", token);
                    cmd.Parameters.AddWithValue("@tf", tf);
                    cmd.ExecuteNonQuery();
                }
            }

            tx.Commit();
        }
        catch (Exception) { /* 읽기 실패 파일 건너뛰기 */ }
    }

    private void RebuildDocFreq()
    {
        using var cmd = _db!.CreateCommand();
        cmd.CommandText = """
            DELETE FROM doc_freq;
            INSERT INTO doc_freq (token, df)
            SELECT token, COUNT(DISTINCT chunk_id) FROM tokens GROUP BY token;
        """;
        cmd.ExecuteNonQuery();
    }

    private int GetTotalChunkCount()
    {
        using var cmd = _db!.CreateCommand();
        cmd.CommandText = "SELECT COUNT(*) FROM chunks";
        return Convert.ToInt32(cmd.ExecuteScalar());
    }

    private void SaveMeta(string key, string value)
    {
        using var cmd = _db!.CreateCommand();
        cmd.CommandText = "INSERT OR REPLACE INTO meta (key, value) VALUES (@k, @v)";
        cmd.Parameters.AddWithValue("@k", key);
        cmd.Parameters.AddWithValue("@v", value);
        cmd.ExecuteNonQuery();
    }
}

/// <summary>인덱싱된 코드 청크.</summary>
public class CodeChunk
{
    public string FilePath { get; init; } = "";
    public int StartLine { get; init; }
    public int EndLine { get; init; }
    public string Content { get; init; } = "";
    public Dictionary<string, int> Tokens { get; init; } = new();
}

/// <summary>검색 결과.</summary>
public class SearchResult
{
    public string FilePath { get; init; } = "";
    public int StartLine { get; init; }
    public int EndLine { get; init; }
    public double Score { get; init; }
    public string Preview { get; init; } = "";
    public override string ToString() => $"{FilePath}:{StartLine}-{EndLine} (score: {Score:F3})";
}
