using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.Json;

namespace AxCopilot.Services;

/// <summary>
/// Language Server Protocol 클라이언트.
/// 외부 언어 서버 프로세스와 JSON-RPC 2.0으로 통신합니다.
/// 지원: OmniSharp (C#), typescript-language-server, pyright, clangd
/// </summary>
public class LspClientService : IDisposable
{
    private Process? _process;
    private StreamWriter? _writer;
    private StreamReader? _reader;
    private int _requestId;
    private bool _initialized;
    private readonly string _language;

    public bool IsConnected => _process is { HasExited: false } && _initialized;

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

    /// <summary>언어에 맞는 LSP 서버를 시작하고 초기화합니다.</summary>
    public async Task<bool> StartAsync(string workspacePath, CancellationToken ct = default)
    {
        var (command, args) = GetServerCommand();
        if (command == null)
        {
            LogService.Warn($"LSP: {_language}에 대한 서버를 찾을 수 없습니다.");
            return false;
        }

        try
        {
            var psi = new ProcessStartInfo
            {
                FileName = command,
                UseShellExecute = false,
                RedirectStandardInput = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                CreateNoWindow = true,
                StandardOutputEncoding = Encoding.UTF8,
                StandardErrorEncoding = Encoding.UTF8,
            };
            foreach (var arg in args)
                psi.ArgumentList.Add(arg);

            _process = Process.Start(psi);
            if (_process == null) return false;

            _writer = _process.StandardInput;
            _reader = _process.StandardOutput;

            // LSP initialize
            var initResult = 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);

            if (initResult == null) return false;

            // initialized notification
            await SendNotificationAsync("initialized", ct);
            _initialized = true;

            LogService.Info($"LSP [{_language}]: 서버 시작 완료 ({command})");
            return true;
        }
        catch (Exception ex)
        {
            LogService.Warn($"LSP [{_language}] 시작 실패: {ex.Message}");
            return false;
        }
    }

    /// <summary>심볼 정의 위치를 찾습니다.</summary>
    public async Task<LspLocation?> GotoDefinitionAsync(string filePath, int line, int character, CancellationToken ct = default)
    {
        var result = await SendRequestAsync("textDocument/definition", new
        {
            textDocument = new { uri = FileToUri(filePath) },
            position = new { line, character }
        }, ct);

        return ParseLocation(result);
    }

    /// <summary>심볼의 모든 참조 위치를 찾습니다.</summary>
    public async Task<List<LspLocation>> FindReferencesAsync(string filePath, int line, int character, CancellationToken ct = default)
    {
        var result = await SendRequestAsync("textDocument/references", new
        {
            textDocument = new { uri = FileToUri(filePath) },
            position = new { line, character },
            context = new { includeDeclaration = true }
        }, ct);

        return ParseLocations(result);
    }

    /// <summary>파일의 심볼 목록을 가져옵니다 (클래스, 메서드, 필드 등).</summary>
    public async Task<List<LspSymbol>> GetDocumentSymbolsAsync(string filePath, CancellationToken ct = default)
    {
        var result = await SendRequestAsync("textDocument/documentSymbol", new
        {
            textDocument = new { uri = FileToUri(filePath) }
        }, ct);

        return ParseSymbols(result);
    }

    // ─── JSON-RPC 통신 ──────────────────────────────────────────────────────

    private static readonly JsonSerializerOptions JsonOpts = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
    };

    private async Task<JsonElement?> SendRequestAsync(string method, object parameters, CancellationToken ct)
    {
        if (_writer == null || _reader == null) return null;

        var id = Interlocked.Increment(ref _requestId);
        var request = new { jsonrpc = "2.0", id, method, @params = parameters };
        var json = JsonSerializer.Serialize(request, JsonOpts);
        var content = Encoding.UTF8.GetBytes(json);

        // LSP 프로토콜: Content-Length 헤더 + \r\n\r\n + 본문
        var 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 var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        cts.CancelAfter(TimeSpan.FromSeconds(15));

        while (!cts.Token.IsCancellationRequested)
        {
            var headerLine = await ReadHeaderAsync(cts.Token);
            if (headerLine == null) continue;

            var bodyLength = ParseContentLength(headerLine);
            if (bodyLength <= 0) continue;

            var body = new byte[bodyLength];
            int read = 0;
            while (read < bodyLength)
            {
                var n = await _reader.BaseStream.ReadAsync(body.AsMemory(read, bodyLength - read), cts.Token);
                if (n == 0) break;
                read += n;
            }

            var responseJson = Encoding.UTF8.GetString(body);
            try
            {
                var doc = JsonDocument.Parse(responseJson);
                var root = doc.RootElement;
                if (!root.TryGetProperty("id", out _)) continue; // notification skip
                if (root.TryGetProperty("result", out var result)) return result;
                if (root.TryGetProperty("error", out var error))
                {
                    var msg = error.TryGetProperty("message", out var m) ? m.GetString() : "Unknown";
                    LogService.Warn($"LSP RPC 오류: {msg}");
                    return null;
                }
            }
            catch (Exception) { continue; }
        }

        return null;
    }

    private async Task SendNotificationAsync(string method, CancellationToken ct)
    {
        if (_writer == null) return;
        var notification = new { jsonrpc = "2.0", method, @params = new { } };
        var json = JsonSerializer.Serialize(notification, JsonOpts);
        var content = Encoding.UTF8.GetBytes(json);
        var 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)
    {
        var sb = new StringBuilder();
        var buf = new byte[1];
        var prevCr = false;
        int emptyLines = 0;

        while (!ct.IsCancellationRequested)
        {
            var n = await _reader!.BaseStream.ReadAsync(buf, ct);
            if (n == 0) return null;

            var ch = (char)buf[0];
            if (ch == '\r') { prevCr = true; continue; }
            if (ch == '\n')
            {
                if (prevCr && sb.Length > 0) return sb.ToString();
                if (prevCr) { emptyLines++; if (emptyLines >= 1 && sb.Length > 0) return sb.ToString(); }
                prevCr = false;
                continue;
            }
            prevCr = false;
            sb.Append(ch);
        }
        return null;
    }

    private static int ParseContentLength(string header)
    {
        const string prefix = "Content-Length:";
        if (header.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
            if (int.TryParse(header[prefix.Length..].Trim(), out var len))
                return len;
        return 0;
    }

    // ─── 결과 파싱 ──────────────────────────────────────────────────────────

    private static LspLocation? ParseLocation(JsonElement? result)
    {
        if (result == null) return null;
        try
        {
            // 단일 Location 또는 배열의 첫 번째
            var elem = result.Value;
            if (elem.ValueKind == JsonValueKind.Array && elem.GetArrayLength() > 0)
                elem = elem[0];
            if (elem.TryGetProperty("uri", out var uri) && elem.TryGetProperty("range", out var range))
            {
                var start = range.GetProperty("start");
                return new LspLocation
                {
                    FilePath = UriToFile(uri.GetString() ?? ""),
                    Line = start.GetProperty("line").GetInt32(),
                    Character = start.GetProperty("character").GetInt32(),
                };
            }
        }
        catch (Exception) { }
        return null;
    }

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

    private static List<LspSymbol> ParseSymbols(JsonElement? result)
    {
        var list = new List<LspSymbol>();
        if (result?.ValueKind != JsonValueKind.Array) return list;
        foreach (var elem in result.Value.EnumerateArray())
        {
            var name = elem.TryGetProperty("name", out var n) ? n.GetString() ?? "" : "";
            var kind = elem.TryGetProperty("kind", out var k) ? SymbolKindName(k.GetInt32()) : "unknown";
            var line = 0;
            if (elem.TryGetProperty("range", out var range))
                line = range.GetProperty("start").GetProperty("line").GetInt32();
            else if (elem.TryGetProperty("location", out var loc))
                line = loc.GetProperty("range").GetProperty("start").GetProperty("line").GetInt32();

            list.Add(new LspSymbol { Name = name, Kind = kind, Line = line });

            // 하위 심볼 (children)
            if (elem.TryGetProperty("children", out var children))
            {
                var childList = ParseSymbols(children);
                foreach (var child in childList)
                    list.Add(new LspSymbol { Name = $"  {child.Name}", Kind = child.Kind, Line = child.Line });
            }
        }
        return list;
    }

    private static string SymbolKindName(int kind) => 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"
    };

    // ─── 유틸 ────────────────────────────────────────────────────────────────

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

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

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

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

    public void Dispose()
    {
        try
        {
            _writer?.Dispose();
            _reader?.Dispose();
            if (_process is { HasExited: false })
            {
                _process.Kill(entireProcessTree: true);
                _process.Dispose();
            }
        }
        catch (Exception) { }
    }
}

/// <summary>LSP 위치 정보.</summary>
public class LspLocation
{
    public string FilePath { get; init; } = "";
    public int Line { get; init; }
    public int Character { get; init; }
    public override string ToString() => $"{FilePath}:{Line + 1}:{Character + 1}";
}

/// <summary>LSP 심볼 정보.</summary>
public class LspSymbol
{
    public string Name { get; init; } = "";
    public string Kind { get; init; } = "";
    public int Line { get; init; }
    public override string ToString() => $"[{Kind}] {Name} (line {Line + 1})";
}
