using System.Text.Json;

namespace ErsatzTV.Mcp;

public sealed class McpServer(IToolExecutor toolExecutor)
{
    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);

    // Process-lifetime document so NullId stays valid; used as the JSON-RPC id for
    // parse errors / requests with no usable id.
    private static readonly JsonDocument NullIdDocument = JsonDocument.Parse("null");
    private static readonly JsonElement NullId = NullIdDocument.RootElement;

    // Process-lifetime empty-arguments document, reused for tool calls that omit "arguments" — so a
    // no-arg call doesn't leak a pooled JsonDocument per invocation.
    private static readonly JsonDocument EmptyArgsDocument = JsonDocument.Parse("{}");
    private static readonly JsonElement EmptyArgs = EmptyArgsDocument.RootElement;

    public async Task<string?> HandleAsync(string requestJson, CancellationToken cancellationToken)
    {
        JsonDocument request;
        try
        {
            request = JsonDocument.Parse(requestJson);
        }
        catch (JsonException)
        {
            // A malformed line must never crash the session loop (JSON-RPC parse error, id null).
            return SerializeError(NullId, -32700, "Parse error: invalid JSON.");
        }

        using (request)
        {
            JsonElement root = request.RootElement;
            JsonElement id = NullId;
            bool hasId = false;
            if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("id", out JsonElement idValue))
            {
                id = idValue;
                hasId = true;
            }

            try
            {
                if (root.ValueKind != JsonValueKind.Object)
                {
                    throw new JsonRpcException(-32600, "Invalid Request: expected a JSON-RPC object.");
                }

                if (!root.TryGetProperty("method", out JsonElement methodElement)
                    || methodElement.ValueKind != JsonValueKind.String)
                {
                    throw new JsonRpcException(-32600, "Invalid Request: missing or non-string 'method'.");
                }

                // No id ⇒ notification ⇒ no response.
                if (!hasId)
                {
                    return null;
                }

                string? method = methodElement.GetString();
                object result = method switch
                {
                    "initialize" => InitializeResult(),
                    "tools/list" => ToolsListResult(),
                    "tools/call" => await CallToolAsync(RequireParams(root), cancellationToken),
                    _ => throw new JsonRpcException(-32601, $"Method not found: {method}")
                };

                return SerializeResponse(id, result);
            }
            catch (JsonRpcException ex)
            {
                return SerializeError(id, ex.Code, ex.Message);
            }
            catch (Exception ex) when (ex is ArgumentException or KeyNotFoundException or InvalidOperationException)
            {
                return hasId ? SerializeError(id, -32602, ex.Message) : null;
            }
            catch (Exception ex)
            {
                // Transport/timeout/unexpected failures (HttpRequestException, a fired request
                // timeout, etc.) must still return a JSON-RPC error for the id — otherwise a
                // compliant client blocks forever awaiting a response that never comes.
                return hasId ? SerializeError(id, -32603, $"Internal error: {ex.Message}") : null;
            }
        }
    }

    private static JsonElement RequireParams(JsonElement root) =>
        root.TryGetProperty("params", out JsonElement parameters)
            ? parameters
            : throw new JsonRpcException(-32602, "Invalid params: missing 'params'.");

    private static object InitializeResult() => new
    {
        protocolVersion = "2024-11-05",
        capabilities = new
        {
            tools = new { }
        },
        serverInfo = new
        {
            name = "ersatztv-mcp",
            version = "0.1.0"
        }
    };

    private static object ToolsListResult() => new
    {
        tools = ToolCatalog.All.Select(t => new
        {
            name = t.Name,
            description = t.Description,
            inputSchema = t.InputSchema.RootElement
        })
    };

    private async Task<object> CallToolAsync(JsonElement parameters, CancellationToken cancellationToken)
    {
        string name = parameters.GetProperty("name").GetString() ?? throw new ArgumentException("Tool name is required");
        ToolDefinition tool = ToolCatalog.Find(name) ?? throw new ArgumentException($"Unknown tool: {name}");
        JsonElement arguments = parameters.TryGetProperty("arguments", out JsonElement args)
            ? args
            : EmptyArgs;

        ToolCallResult result = await toolExecutor.CallToolAsync(tool, arguments, cancellationToken);
        return new
        {
            content = new[]
            {
                new
                {
                    type = "text",
                    text = result.Text
                }
            },
            isError = result.IsError
        };
    }

    private static string SerializeResponse(JsonElement id, object result) =>
        JsonSerializer.Serialize(
            new
            {
                jsonrpc = "2.0",
                id = id.Clone(),
                result
            },
            JsonOptions);

    private static string SerializeError(JsonElement id, int code, string message) =>
        JsonSerializer.Serialize(
            new
            {
                jsonrpc = "2.0",
                id = id.Clone(),
                error = new
                {
                    code,
                    message
                }
            },
            JsonOptions);

    private sealed class JsonRpcException(int code, string message) : Exception(message)
    {
        public int Code { get; } = code;
    }
}
