using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Data.Sqlite;

namespace AxCopilot.Services;

public class CodeIndexService : IDisposable
{
	private SqliteConnection? _db;

	private string _workFolder = "";

	private bool _indexed;

	private int _totalDocs;

	private static readonly HashSet<string> StopWords = new HashSet<string>(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 HashSet<string>(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"
	};

	public bool IsIndexed => _indexed;

	public int ChunkCount => _totalDocs;

	private string GetDbPath(string workFolder)
	{
		string text = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(workFolder.ToLowerInvariant()))).Substring(0, 16);
		string text2 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "index");
		Directory.CreateDirectory(text2);
		return Path.Combine(text2, text + ".db");
	}

	private void EnsureDb(string workFolder)
	{
		if (_db != null && _workFolder == workFolder)
		{
			return;
		}
		_db?.Dispose();
		_workFolder = workFolder;
		string dbPath = GetDbPath(workFolder);
		_db = new SqliteConnection("Data Source=" + dbPath);
		_db.Open();
		using (SqliteCommand sqliteCommand = _db.CreateCommand())
		{
			sqliteCommand.CommandText = "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;";
			sqliteCommand.ExecuteNonQuery();
		}
		using SqliteCommand sqliteCommand2 = _db.CreateCommand();
		sqliteCommand2.CommandText = "    CREATE TABLE IF NOT EXISTS files (\n        id INTEGER PRIMARY KEY AUTOINCREMENT,\n        path TEXT NOT NULL UNIQUE,\n        last_modified TEXT NOT NULL,\n        file_size INTEGER NOT NULL\n    );\n    CREATE TABLE IF NOT EXISTS chunks (\n        id INTEGER PRIMARY KEY AUTOINCREMENT,\n        file_id INTEGER NOT NULL,\n        start_line INTEGER NOT NULL,\n        end_line INTEGER NOT NULL,\n        content TEXT NOT NULL,\n        FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE\n    );\n    CREATE TABLE IF NOT EXISTS tokens (\n        chunk_id INTEGER NOT NULL,\n        token TEXT NOT NULL,\n        tf INTEGER NOT NULL,\n        FOREIGN KEY (chunk_id) REFERENCES chunks(id) ON DELETE CASCADE\n    );\n    CREATE TABLE IF NOT EXISTS doc_freq (\n        token TEXT PRIMARY KEY,\n        df INTEGER NOT NULL\n    );\n    CREATE TABLE IF NOT EXISTS meta (\n        key TEXT PRIMARY KEY,\n        value TEXT NOT NULL\n    );\n    CREATE INDEX IF NOT EXISTS idx_tokens_chunk ON tokens(chunk_id);\n    CREATE INDEX IF NOT EXISTS idx_tokens_token ON tokens(token);\n    CREATE INDEX IF NOT EXISTS idx_chunks_file ON chunks(file_id);";
		sqliteCommand2.ExecuteNonQuery();
	}

	public async Task IndexAsync(string workFolder, CancellationToken ct = default(CancellationToken))
	{
		if (string.IsNullOrEmpty(workFolder) || !Directory.Exists(workFolder))
		{
			return;
		}
		EnsureDb(workFolder);
		await Task.Run(delegate
		{
			Dictionary<string, int> dictionary = LoadExistingFiles();
			int maxFileKb = 500;
			try
			{
				int num = ((!(Application.Current is App app)) ? ((int?)null) : app.SettingsService?.Settings.Llm.Code.CodeIndexMaxFileKb) ?? 500;
				if (num > 0)
				{
					maxFileKb = num;
				}
			}
			catch
			{
			}
			Dictionary<string, FileInfo> dictionary2 = ScanFiles(workFolder, maxFileKb);
			int num2 = 0;
			int num3 = 0;
			int num4 = 0;
			string key;
			foreach (KeyValuePair<string, int> item in dictionary)
			{
				item.Deconstruct(out key, out var value);
				string key2 = key;
				int fileId = value;
				if (!dictionary2.ContainsKey(key2))
				{
					RemoveFileFromIndex(fileId);
					num4++;
				}
			}
			foreach (KeyValuePair<string, FileInfo> item2 in dictionary2)
			{
				item2.Deconstruct(out key, out var value2);
				string text = key;
				FileInfo fileInfo = value2;
				if (ct.IsCancellationRequested)
				{
					break;
				}
				string lastMod = fileInfo.LastWriteTimeUtc.ToString("O");
				long length = fileInfo.Length;
				if (dictionary.TryGetValue(text, out var value3))
				{
					if (!IsFileChanged(value3, lastMod, length))
					{
						continue;
					}
					RemoveFileFromIndex(value3);
					num3++;
				}
				else
				{
					num2++;
				}
				IndexFile(workFolder, text, lastMod, length);
			}
			RebuildDocFreq();
			_totalDocs = GetTotalChunkCount();
			_indexed = _totalDocs > 0;
			SaveMeta("lastIndexed", DateTime.UtcNow.ToString("O"));
			SaveMeta("workFolder", workFolder);
			LogService.Info($"코드 인덱싱 완료: {_totalDocs}개 청크 (추가:{num2} 갱신:{num3} 삭제:{num4}) [{workFolder}]");
		}, ct);
	}

	private Dictionary<string, int> LoadExistingFiles()
	{
		Dictionary<string, int> dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
		using SqliteCommand sqliteCommand = _db.CreateCommand();
		sqliteCommand.CommandText = "SELECT id, path FROM files";
		using SqliteDataReader sqliteDataReader = sqliteCommand.ExecuteReader();
		while (sqliteDataReader.Read())
		{
			dictionary[sqliteDataReader.GetString(1)] = sqliteDataReader.GetInt32(0);
		}
		return dictionary;
	}

	private Dictionary<string, FileInfo> ScanFiles(string workFolder, int maxFileKb = 500)
	{
		Dictionary<string, FileInfo> dictionary = new Dictionary<string, FileInfo>(StringComparer.OrdinalIgnoreCase);
		long num = (long)maxFileKb * 1024L;
		try
		{
			IEnumerable<string> enumerable = Directory.EnumerateFiles(workFolder, "*.*", new EnumerationOptions
			{
				RecurseSubdirectories = true,
				IgnoreInaccessible = true,
				MaxRecursionDepth = 8
			});
			foreach (string item in enumerable)
			{
				string extension = Path.GetExtension(item);
				if (!CodeExtensions.Contains(extension))
				{
					continue;
				}
				try
				{
					FileInfo fileInfo = new FileInfo(item);
					if (fileInfo.Length <= num)
					{
						string relativePath = Path.GetRelativePath(workFolder, item);
						dictionary[relativePath] = fileInfo;
					}
				}
				catch
				{
				}
			}
		}
		catch
		{
		}
		return dictionary;
	}

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

	private void RemoveFileFromIndex(int fileId)
	{
		List<int> list = new List<int>();
		using (SqliteCommand sqliteCommand = _db.CreateCommand())
		{
			sqliteCommand.CommandText = "SELECT id FROM chunks WHERE file_id = @fid";
			sqliteCommand.Parameters.AddWithValue("@fid", fileId);
			using SqliteDataReader sqliteDataReader = sqliteCommand.ExecuteReader();
			while (sqliteDataReader.Read())
			{
				list.Add(sqliteDataReader.GetInt32(0));
			}
		}
		foreach (int item in list)
		{
			using SqliteCommand sqliteCommand2 = _db.CreateCommand();
			sqliteCommand2.CommandText = "DELETE FROM tokens WHERE chunk_id = @cid";
			sqliteCommand2.Parameters.AddWithValue("@cid", item);
			sqliteCommand2.ExecuteNonQuery();
		}
		using (SqliteCommand sqliteCommand3 = _db.CreateCommand())
		{
			sqliteCommand3.CommandText = "DELETE FROM chunks WHERE file_id = @fid";
			sqliteCommand3.Parameters.AddWithValue("@fid", fileId);
			sqliteCommand3.ExecuteNonQuery();
		}
		using SqliteCommand sqliteCommand4 = _db.CreateCommand();
		sqliteCommand4.CommandText = "DELETE FROM files WHERE id = @fid";
		sqliteCommand4.Parameters.AddWithValue("@fid", fileId);
		sqliteCommand4.ExecuteNonQuery();
	}

	private void IndexFile(string workFolder, string relPath, string lastMod, long size)
	{
		try
		{
			string path = Path.Combine(workFolder, relPath);
			string text = File.ReadAllText(path, Encoding.UTF8);
			int num;
			using (SqliteCommand sqliteCommand = _db.CreateCommand())
			{
				sqliteCommand.CommandText = "INSERT INTO files (path, last_modified, file_size) VALUES (@p, @m, @s) RETURNING id";
				sqliteCommand.Parameters.AddWithValue("@p", relPath);
				sqliteCommand.Parameters.AddWithValue("@m", lastMod);
				sqliteCommand.Parameters.AddWithValue("@s", size);
				num = Convert.ToInt32(sqliteCommand.ExecuteScalar());
			}
			string[] array = text.Split('\n');
			using SqliteTransaction sqliteTransaction = _db.BeginTransaction();
			for (int i = 0; i < array.Length; i += 50)
			{
				string text2 = string.Join("\n", array.AsSpan(i, Math.Min(50, array.Length - i)).ToArray());
				if (string.IsNullOrWhiteSpace(text2))
				{
					continue;
				}
				int num2 = Math.Min(i + 50, array.Length);
				int num3;
				using (SqliteCommand sqliteCommand2 = _db.CreateCommand())
				{
					sqliteCommand2.Transaction = sqliteTransaction;
					sqliteCommand2.CommandText = "INSERT INTO chunks (file_id, start_line, end_line, content) VALUES (@f, @s, @e, @c) RETURNING id";
					sqliteCommand2.Parameters.AddWithValue("@f", num);
					sqliteCommand2.Parameters.AddWithValue("@s", i + 1);
					sqliteCommand2.Parameters.AddWithValue("@e", num2);
					sqliteCommand2.Parameters.AddWithValue("@c", text2);
					num3 = Convert.ToInt32(sqliteCommand2.ExecuteScalar());
				}
				Dictionary<string, int> dictionary = Tokenize(text2);
				foreach (var (value, num5) in dictionary)
				{
					using SqliteCommand sqliteCommand3 = _db.CreateCommand();
					sqliteCommand3.Transaction = sqliteTransaction;
					sqliteCommand3.CommandText = "INSERT INTO tokens (chunk_id, token, tf) VALUES (@cid, @t, @tf)";
					sqliteCommand3.Parameters.AddWithValue("@cid", num3);
					sqliteCommand3.Parameters.AddWithValue("@t", value);
					sqliteCommand3.Parameters.AddWithValue("@tf", num5);
					sqliteCommand3.ExecuteNonQuery();
				}
			}
			sqliteTransaction.Commit();
		}
		catch
		{
		}
	}

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

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

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

	public List<SearchResult> Search(string query, int maxResults = 5)
	{
		if (!_indexed || _db == null || _totalDocs == 0)
		{
			return new List<SearchResult>();
		}
		Dictionary<string, int> dictionary = Tokenize(query);
		if (dictionary.Count == 0)
		{
			return new List<SearchResult>();
		}
		Dictionary<string, int> dictionary2 = new Dictionary<string, int>();
		foreach (string key in dictionary.Keys)
		{
			using SqliteCommand sqliteCommand = _db.CreateCommand();
			sqliteCommand.CommandText = "SELECT df FROM doc_freq WHERE token = @t";
			sqliteCommand.Parameters.AddWithValue("@t", key);
			object obj = sqliteCommand.ExecuteScalar();
			if (obj != null)
			{
				dictionary2[key] = Convert.ToInt32(obj);
			}
		}
		HashSet<int> hashSet = new HashSet<int>();
		foreach (string key2 in dictionary.Keys)
		{
			using SqliteCommand sqliteCommand2 = _db.CreateCommand();
			sqliteCommand2.CommandText = "SELECT DISTINCT chunk_id FROM tokens WHERE token = @t";
			sqliteCommand2.Parameters.AddWithValue("@t", key2);
			using SqliteDataReader sqliteDataReader = sqliteCommand2.ExecuteReader();
			while (sqliteDataReader.Read())
			{
				hashSet.Add(sqliteDataReader.GetInt32(0));
			}
		}
		if (hashSet.Count == 0)
		{
			return new List<SearchResult>();
		}
		List<(int, double)> list = new List<(int, double)>();
		foreach (int item in hashSet)
		{
			Dictionary<string, int> dictionary3 = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
			using (SqliteCommand sqliteCommand3 = _db.CreateCommand())
			{
				sqliteCommand3.CommandText = "SELECT token, tf FROM tokens WHERE chunk_id = @cid";
				sqliteCommand3.Parameters.AddWithValue("@cid", item);
				using SqliteDataReader sqliteDataReader2 = sqliteCommand3.ExecuteReader();
				while (sqliteDataReader2.Read())
				{
					dictionary3[sqliteDataReader2.GetString(0)] = sqliteDataReader2.GetInt32(1);
				}
			}
			double num = ComputeTfIdfSimilarity(dictionary, dictionary3, dictionary2);
			if (num > 0.01)
			{
				list.Add((item, num));
			}
		}
		List<(int, double)> list2 = list.OrderByDescending<(int, double), double>(((int ChunkId, double Score) s) => s.Score).Take(maxResults).ToList();
		List<SearchResult> list3 = new List<SearchResult>();
		foreach (var (num2, score) in list2)
		{
			using SqliteCommand sqliteCommand4 = _db.CreateCommand();
			sqliteCommand4.CommandText = "    SELECT f.path, c.start_line, c.end_line, c.content\n    FROM chunks c JOIN files f ON c.file_id = f.id\n    WHERE c.id = @cid";
			sqliteCommand4.Parameters.AddWithValue("@cid", num2);
			using SqliteDataReader sqliteDataReader3 = sqliteCommand4.ExecuteReader();
			if (sqliteDataReader3.Read())
			{
				SearchResult obj2 = new SearchResult
				{
					FilePath = sqliteDataReader3.GetString(0),
					StartLine = sqliteDataReader3.GetInt32(1),
					EndLine = sqliteDataReader3.GetInt32(2),
					Score = score
				};
				string text = sqliteDataReader3.GetString(3);
				obj2.Preview = ((text != null && text.Length > 200) ? (text.Substring(0, 200) + "...") : sqliteDataReader3.GetString(3));
				list3.Add(obj2);
			}
		}
		return list3;
	}

	public void TryLoadExisting(string workFolder)
	{
		if (string.IsNullOrEmpty(workFolder) || !Directory.Exists(workFolder))
		{
			return;
		}
		string dbPath = GetDbPath(workFolder);
		if (File.Exists(dbPath))
		{
			EnsureDb(workFolder);
			_totalDocs = GetTotalChunkCount();
			_indexed = _totalDocs > 0;
			if (_indexed)
			{
				LogService.Info($"기존 코드 인덱스 로드: {_totalDocs}개 청크 [{workFolder}]");
			}
		}
	}

	private double ComputeTfIdfSimilarity(Dictionary<string, int> queryTf, Dictionary<string, int> docTf, Dictionary<string, int> dfMap)
	{
		double num = 0.0;
		double num2 = 0.0;
		double num3 = 0.0;
		string key;
		int value;
		foreach (KeyValuePair<string, int> item in queryTf)
		{
			item.Deconstruct(out key, out value);
			string key2 = key;
			int num4 = value;
			int valueOrDefault = dfMap.GetValueOrDefault(key2, 0);
			double num5 = Math.Log(1.0 + (double)_totalDocs / (1.0 + (double)valueOrDefault));
			double num6 = (double)num4 * num5;
			num2 += num6 * num6;
			if (docTf.TryGetValue(key2, out var value2))
			{
				double num7 = (double)value2 * num5;
				num += num6 * num7;
			}
		}
		foreach (KeyValuePair<string, int> item2 in docTf)
		{
			item2.Deconstruct(out key, out value);
			string key3 = key;
			int num8 = value;
			int valueOrDefault2 = dfMap.GetValueOrDefault(key3, 0);
			double num9 = Math.Log(1.0 + (double)_totalDocs / (1.0 + (double)valueOrDefault2));
			double num10 = (double)num8 * num9;
			num3 += num10 * num10;
		}
		if (num2 == 0.0 || num3 == 0.0)
		{
			return 0.0;
		}
		return num / (Math.Sqrt(num2) * Math.Sqrt(num3));
	}

	private static Dictionary<string, int> Tokenize(string text)
	{
		Dictionary<string, int> dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
		IEnumerable<string> enumerable = from w in Regex.Split(text, "[^a-zA-Z0-9가-힣_]+").SelectMany(SplitCamelCase)
			where w.Length >= 2 && !StopWords.Contains(w)
			select w;
		foreach (string item in enumerable)
		{
			string key = item.ToLowerInvariant();
			dictionary.TryGetValue(key, out var value);
			dictionary[key] = value + 1;
		}
		List<string> list = enumerable.Select((string w) => w.ToLowerInvariant()).ToList();
		for (int num = 0; num < list.Count - 1; num++)
		{
			string key2 = list[num] + "_" + list[num + 1];
			dictionary.TryGetValue(key2, out var value2);
			dictionary[key2] = value2 + 1;
		}
		return dictionary;
	}

	private static IEnumerable<string> SplitCamelCase(string word)
	{
		if (string.IsNullOrEmpty(word))
		{
			yield break;
		}
		StringBuilder sb = new StringBuilder();
		foreach (char ch in word)
		{
			if (char.IsUpper(ch) && sb.Length > 0)
			{
				yield return sb.ToString();
				sb.Clear();
			}
			sb.Append(ch);
		}
		if (sb.Length > 0)
		{
			yield return sb.ToString();
		}
	}

	public void Dispose()
	{
		_db?.Dispose();
		_db = null;
	}
}
