using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace AxCopilot.Services;

public class LspClientService : IDisposable
{
	private Process? _process;

	private StreamWriter? _writer;

	private StreamReader? _reader;

	private int _requestId;

	private bool _initialized;

	private readonly string _language;

	private static readonly JsonSerializerOptions JsonOpts = new JsonSerializerOptions
	{
		PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
		Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
	};

	public bool IsConnected
	{
		get
		{
			Process process = _process;
			return process != null && !process.HasExited && _initialized;
		}
	}

	public LspClientService(string language)
	{
		_language = language.ToLowerInvariant();
	}

	public async Task<bool> StartAsync(string workspacePath, CancellationToken ct = default(CancellationToken))
	{
		string command;
		string[] args;
		(command, args) = GetServerCommand();
		if (command == null)
		{
			LogService.Warn("LSP: " + _language + "에 대한 서버를 찾을 수 없습니다.");
			return false;
		}
		try
		{
			ProcessStartInfo psi = new ProcessStartInfo
			{
				FileName = command,
				UseShellExecute = false,
				RedirectStandardInput = true,
				RedirectStandardOutput = true,
				RedirectStandardError = true,
				CreateNoWindow = true,
				StandardOutputEncoding = Encoding.UTF8,
				StandardErrorEncoding = Encoding.UTF8
			};
			string[] array = args;
			foreach (string arg in array)
			{
				psi.ArgumentList.Add(arg);
			}
			_process = Process.Start(psi);
			if (_process == null)
			{
				return false;
			}
			_writer = _process.StandardInput;
			_reader = _process.StandardOutput;
			if (!(await SendRequestAsync("initialize", new
			{
				processId = Environment.ProcessId,
				rootUri = "file:///" + workspacePath.Replace('\\', '/').TrimStart('/'),
				capabilities = new
				{
					textDocument = new
					{
						definition = new
						{
							dynamicRegistration = false
						},
						references = new
						{
							dynamicRegistration = false
						},
						documentSymbol = new
						{
							dynamicRegistration = false
						}
					}
				}
			}, ct)).HasValue)
			{
				return false;
			}
			await SendNotificationAsync("initialized", ct);
			_initialized = true;
			LogService.Info($"LSP [{_language}]: 서버 시작 완료 ({command})");
			return true;
		}
		catch (Exception ex)
		{
			LogService.Warn("LSP [" + _language + "] 시작 실패: " + ex.Message);
			return false;
		}
	}

	public async Task<LspLocation?> GotoDefinitionAsync(string filePath, int line, int character, CancellationToken ct = default(CancellationToken))
	{
		return ParseLocation(await SendRequestAsync("textDocument/definition", new
		{
			textDocument = new
			{
				uri = FileToUri(filePath)
			},
			position = new { line, character }
		}, ct));
	}

	public async Task<List<LspLocation>> FindReferencesAsync(string filePath, int line, int character, CancellationToken ct = default(CancellationToken))
	{
		return ParseLocations(await SendRequestAsync("textDocument/references", new
		{
			textDocument = new
			{
				uri = FileToUri(filePath)
			},
			position = new { line, character },
			context = new
			{
				includeDeclaration = true
			}
		}, ct));
	}

	public async Task<List<LspSymbol>> GetDocumentSymbolsAsync(string filePath, CancellationToken ct = default(CancellationToken))
	{
		return ParseSymbols(await SendRequestAsync("textDocument/documentSymbol", new
		{
			textDocument = new
			{
				uri = FileToUri(filePath)
			}
		}, ct));
	}

	private async Task<JsonElement?> SendRequestAsync(string method, object parameters, CancellationToken ct)
	{
		if (_writer == null || _reader == null)
		{
			return null;
		}
		int id = Interlocked.Increment(ref _requestId);
		var request = new
		{
			jsonrpc = "2.0",
			id = id,
			method = method,
			@params = parameters
		};
		string json = JsonSerializer.Serialize(request, JsonOpts);
		byte[] content = Encoding.UTF8.GetBytes(json);
		string header = $"Content-Length: {content.Length}\r\n\r\n";
		await _writer.BaseStream.WriteAsync(Encoding.ASCII.GetBytes(header), ct);
		await _writer.BaseStream.WriteAsync(content, ct);
		await _writer.BaseStream.FlushAsync(ct);
		using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
		cts.CancelAfter(TimeSpan.FromSeconds(15.0));
		while (!cts.Token.IsCancellationRequested)
		{
			string headerLine = await ReadHeaderAsync(cts.Token);
			if (headerLine == null)
			{
				continue;
			}
			int bodyLength = ParseContentLength(headerLine);
			if (bodyLength <= 0)
			{
				continue;
			}
			byte[] body = new byte[bodyLength];
			int n;
			for (int read = 0; read < bodyLength; read += n)
			{
				n = await _reader.BaseStream.ReadAsync(body.AsMemory(read, bodyLength - read), cts.Token);
				if (n == 0)
				{
					break;
				}
			}
			string responseJson = Encoding.UTF8.GetString(body);
			try
			{
				JsonDocument doc = JsonDocument.Parse(responseJson);
				JsonElement root = doc.RootElement;
				if (root.TryGetProperty("id", out var _))
				{
					if (root.TryGetProperty("result", out var result))
					{
						return result;
					}
					if (root.TryGetProperty("error", out var error))
					{
						JsonElement m;
						string msg = (error.TryGetProperty("message", out m) ? m.GetString() : "Unknown");
						LogService.Warn("LSP RPC 오류: " + msg);
						return null;
					}
					result = default(JsonElement);
					error = default(JsonElement);
				}
			}
			catch
			{
			}
		}
		return null;
	}

	private async Task SendNotificationAsync(string method, CancellationToken ct)
	{
		if (_writer != null)
		{
			var notification = new
			{
				jsonrpc = "2.0",
				method = method,
				@params = new { }
			};
			string json = JsonSerializer.Serialize(notification, JsonOpts);
			byte[] content = Encoding.UTF8.GetBytes(json);
			string header = $"Content-Length: {content.Length}\r\n\r\n";
			await _writer.BaseStream.WriteAsync(Encoding.ASCII.GetBytes(header), ct);
			await _writer.BaseStream.WriteAsync(content, ct);
			await _writer.BaseStream.FlushAsync(ct);
		}
	}

	private async Task<string?> ReadHeaderAsync(CancellationToken ct)
	{
		StringBuilder sb = new StringBuilder();
		byte[] buf = new byte[1];
		bool prevCr = false;
		int emptyLines = 0;
		while (!ct.IsCancellationRequested)
		{
			if (await _reader.BaseStream.ReadAsync(buf, ct) == 0)
			{
				return null;
			}
			char ch = (char)buf[0];
			switch (ch)
			{
			case '\r':
				prevCr = true;
				break;
			case '\n':
				if (prevCr && sb.Length > 0)
				{
					return sb.ToString();
				}
				if (prevCr)
				{
					emptyLines++;
					if (emptyLines >= 1 && sb.Length > 0)
					{
						return sb.ToString();
					}
				}
				prevCr = false;
				break;
			default:
				prevCr = false;
				sb.Append(ch);
				break;
			}
		}
		return null;
	}

	private static int ParseContentLength(string header)
	{
		if (header.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase))
		{
			int length = "Content-Length:".Length;
			if (int.TryParse(header.Substring(length, header.Length - length).Trim(), out var result))
			{
				return result;
			}
		}
		return 0;
	}

	private static LspLocation? ParseLocation(JsonElement? result)
	{
		if (!result.HasValue)
		{
			return null;
		}
		try
		{
			JsonElement jsonElement = result.Value;
			if (jsonElement.ValueKind == JsonValueKind.Array && jsonElement.GetArrayLength() > 0)
			{
				jsonElement = jsonElement[0];
			}
			if (jsonElement.TryGetProperty("uri", out var value) && jsonElement.TryGetProperty("range", out var value2))
			{
				JsonElement property = value2.GetProperty("start");
				return new LspLocation
				{
					FilePath = UriToFile(value.GetString() ?? ""),
					Line = property.GetProperty("line").GetInt32(),
					Character = property.GetProperty("character").GetInt32()
				};
			}
		}
		catch
		{
		}
		return null;
	}

	private static List<LspLocation> ParseLocations(JsonElement? result)
	{
		List<LspLocation> list = new List<LspLocation>();
		if (!result.HasValue || result.GetValueOrDefault().ValueKind != JsonValueKind.Array)
		{
			return list;
		}
		foreach (JsonElement item in result.Value.EnumerateArray())
		{
			if (item.TryGetProperty("uri", out var value) && item.TryGetProperty("range", out var value2))
			{
				JsonElement property = value2.GetProperty("start");
				list.Add(new LspLocation
				{
					FilePath = UriToFile(value.GetString() ?? ""),
					Line = property.GetProperty("line").GetInt32(),
					Character = property.GetProperty("character").GetInt32()
				});
			}
		}
		return list;
	}

	private static List<LspSymbol> ParseSymbols(JsonElement? result)
	{
		List<LspSymbol> list = new List<LspSymbol>();
		if (!result.HasValue || result.GetValueOrDefault().ValueKind != JsonValueKind.Array)
		{
			return list;
		}
		foreach (JsonElement item in result.Value.EnumerateArray())
		{
			JsonElement value;
			string name = (item.TryGetProperty("name", out value) ? (value.GetString() ?? "") : "");
			JsonElement value2;
			string kind = (item.TryGetProperty("kind", out value2) ? SymbolKindName(value2.GetInt32()) : "unknown");
			int line = 0;
			JsonElement value4;
			if (item.TryGetProperty("range", out var value3))
			{
				line = value3.GetProperty("start").GetProperty("line").GetInt32();
			}
			else if (item.TryGetProperty("location", out value4))
			{
				line = value4.GetProperty("range").GetProperty("start").GetProperty("line")
					.GetInt32();
			}
			list.Add(new LspSymbol
			{
				Name = name,
				Kind = kind,
				Line = line
			});
			if (!item.TryGetProperty("children", out var value5))
			{
				continue;
			}
			List<LspSymbol> list2 = ParseSymbols(value5);
			foreach (LspSymbol item2 in list2)
			{
				list.Add(new LspSymbol
				{
					Name = "  " + item2.Name,
					Kind = item2.Kind,
					Line = item2.Line
				});
			}
		}
		return list;
	}

	private static string SymbolKindName(int kind)
	{
		if (1 == 0)
		{
		}
		string result = kind switch
		{
			1 => "file", 
			2 => "module", 
			3 => "namespace", 
			4 => "package", 
			5 => "class", 
			6 => "method", 
			7 => "property", 
			8 => "field", 
			9 => "constructor", 
			10 => "enum", 
			11 => "interface", 
			12 => "function", 
			13 => "variable", 
			14 => "constant", 
			23 => "struct", 
			24 => "event", 
			_ => "symbol", 
		};
		if (1 == 0)
		{
		}
		return result;
	}

	private static string FileToUri(string path)
	{
		return "file:///" + path.Replace('\\', '/').TrimStart('/');
	}

	private static string UriToFile(string uri)
	{
		if (uri.StartsWith("file:///"))
		{
			return uri.Substring(8, uri.Length - 8).Replace('/', '\\');
		}
		return uri;
	}

	private (string? Command, string[] Args) GetServerCommand()
	{
		string language = _language;
		if (1 == 0)
		{
		}
		(string, string[]) result;
		switch (language)
		{
		case "csharp":
		case "c#":
			result = FindCommand("OmniSharp", new string[2] { "omnisharp", "OmniSharp.exe" }, new string[1] { "--languageserver" });
			break;
		case "typescript":
		case "javascript":
		case "ts":
		case "js":
			result = FindCommand("TypeScript", new string[1] { "typescript-language-server" }, new string[1] { "--stdio" });
			break;
		case "python":
		case "py":
			result = FindCommand("Python", new string[2] { "pyright-langserver", "pylsp" }, new string[1] { "--stdio" });
			break;
		case "cpp":
		case "c++":
		case "c":
			result = FindCommand("C/C++", new string[1] { "clangd" }, Array.Empty<string>());
			break;
		case "java":
			result = FindCommand("Java", new string[1] { "jdtls" }, Array.Empty<string>());
			break;
		default:
			result = (null, Array.Empty<string>());
			break;
		}
		if (1 == 0)
		{
		}
		return result;
	}

	private static (string? Command, string[] Args) FindCommand(string label, string[] candidates, string[] defaultArgs)
	{
		foreach (string text in candidates)
		{
			try
			{
				using Process process = Process.Start(new ProcessStartInfo
				{
					FileName = ((Environment.OSVersion.Platform == PlatformID.Win32NT) ? "where" : "which"),
					Arguments = text,
					UseShellExecute = false,
					RedirectStandardOutput = true,
					CreateNoWindow = true
				});
				process?.WaitForExit(3000);
				if (process != null && process.ExitCode == 0)
				{
					LogService.Info($"LSP [{label}]: {text} 발견");
					return (Command: text, Args: defaultArgs);
				}
			}
			catch (Exception ex)
			{
				LogService.Debug($"LSP [{label}]: {text} 탐색 실패 — {ex.Message}");
			}
		}
		return (Command: null, Args: Array.Empty<string>());
	}

	public void Dispose()
	{
		try
		{
			_writer?.Dispose();
			_reader?.Dispose();
			Process process = _process;
			if (process != null && !process.HasExited)
			{
				_process.Kill(entireProcessTree: true);
				_process.Dispose();
			}
		}
		catch
		{
		}
	}
}
