using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Hc900Crawler.Infrastructure.Mcp;

/// <summary>
/// Python FastMCP 서버 (localhost:5001)와 JSON-RPC over HTTP로 통신하는 저수준 클라이언트.
/// 모델 클래스(McpResponse 등)도 여기서 단일 관리한다.
/// </summary>
public class McpClient
{
    public const string HttpClientName = "McpClient";
    // 소프트 타임아웃 — 1차 시도 한계. 초과 시 onTimeoutExtended 콜백 1회 후 연장.
    public const int SoftTimeoutSeconds = 120;
    // 연장 타임아웃 — 2차 시도 한계. 이걸 초과해야 실제 TimeoutException.
    public const int ExtendedTimeoutSeconds = 300;

    private readonly HttpClient _httpClient;

    private static readonly JsonSerializerOptions _jsonOptions = new()
    {
        PropertyNameCaseInsensitive = true
    };

    // 운영 경로: IHttpClientFactory에서 BaseAddress/Timeout이 설정된 클라이언트 주입
    public McpClient(IHttpClientFactory httpClientFactory)
    {
        _httpClient = httpClientFactory.CreateClient(HttpClientName);
    }

    public async Task<bool> PingAsync()
    {
        try
        {
            // FastMCP는 /health 대신 /mcp 엔드포인트를 제공함
            // 406은 Accept 헤더 문제이지만, MCP 서버가 실행 중이라는 의미
            var response = await _httpClient.GetAsync("/mcp");
            return response.IsSuccessStatusCode || response.StatusCode == System.Net.HttpStatusCode.NotAcceptable;
        }
        catch
        {
            return false;
        }
    }

    public async Task<List<McpTool>> ListToolsAsync(CancellationToken ct = default)
    {
        var request = new
        {
            jsonrpc = "2.0",
            id = Guid.NewGuid().ToString(),
            method = "tools/list"
        };

        var response = await SendRequestAsync(request, TimeSpan.FromSeconds(SoftTimeoutSeconds), ct);
        if (response?.result?.tools == null)
            return [];

        return [.. response.result.tools];
    }

    /// <summary>
    /// MCP 도구 호출. 1차 시도(SoftTimeoutSeconds) 타임아웃 시 onTimeoutExtended 콜백 후
    /// 2차 시도(ExtendedTimeoutSeconds)로 자동 연장. 사용자가 ct로 취소한 경우는 연장하지 않음.
    /// </summary>
    public async Task<string> CallToolAsync(
        string toolName,
        Dictionary<string, object> arguments,
        CancellationToken ct = default,
        Func<TimeoutExtendInfo, Task>? onTimeoutExtended = null)
    {
        var request = new
        {
            jsonrpc = "2.0",
            id = Guid.NewGuid().ToString(),
            method = "tools/call",
            @params = new { name = toolName, arguments = arguments }
        };

        try
        {
            McpResponse? response;
            try
            {
                response = await SendRequestAsync(request, TimeSpan.FromSeconds(SoftTimeoutSeconds), ct);
            }
            catch (OperationCanceledException) when (!ct.IsCancellationRequested)
            {
                // 소프트 타임아웃 — 사용자 취소 아님. 1회 연장 시도.
                if (onTimeoutExtended != null)
                {
                    try { await onTimeoutExtended(new TimeoutExtendInfo(toolName, SoftTimeoutSeconds, ExtendedTimeoutSeconds)); }
                    catch { /* 콜백 실패는 본 호출에 영향 X */ }
                }
                response = await SendRequestAsync(request, TimeSpan.FromSeconds(ExtendedTimeoutSeconds), ct);
            }

            var content = response?.result?.content;
            if (content == null || content.Length == 0)
                return "호출 결과 없음";

            var sb = new StringBuilder();
            foreach (var item in content)
            {
                if (item.type == "text")
                    sb.AppendLine(item.text);
                else if (item.type == "image")
                    sb.AppendLine($"[이미지: {item.data ?? "blob"}]");
            }
            return sb.Length > 0 ? sb.ToString().TrimEnd() : "호출 결과 없음";
        }
        catch (OperationCanceledException) when (!ct.IsCancellationRequested)
        {
            return $"도구 호출 실패: 연장 타임아웃({ExtendedTimeoutSeconds}초) 초과";
        }
        catch (Exception ex)
        {
            return $"도구 호출 실패: {ex.Message}";
        }
    }

    public Task<string> RunSqlAsync(string sql, CancellationToken ct = default) =>
        CallToolAsync("run_sql", new Dictionary<string, object> { ["sql"] = sql }, ct);

    public Task<string> QueryPvHistoryAsync(
        List<string> tagNames, string timeFrom, string timeTo, int limit = 100, CancellationToken ct = default) =>
        CallToolAsync("query_pv_history", new Dictionary<string, object>
        {
            ["tag_names"] = tagNames,
            ["time_from"] = timeFrom,
            ["time_to"] = timeTo,
            ["limit"] = limit
        }, ct);

    public Task<string> GetTagMetadataAsync(string query, int limit = 10, CancellationToken ct = default) =>
        CallToolAsync("get_tag_metadata", new Dictionary<string, object>
        {
            ["query"] = query,
            ["limit"] = limit
        }, ct);

    public Task<string> ListDrawingsAsync(string? unitNo = null, CancellationToken ct = default)
    {
        var args = new Dictionary<string, object>();
        if (!string.IsNullOrEmpty(unitNo))
            args["unit_no"] = unitNo;
        return CallToolAsync("list_drawings", args, ct);
    }

    public Task<string> QueryWithNlAsync(string question, CancellationToken ct = default) =>
        CallToolAsync("query_with_nl", new Dictionary<string, object> { ["question"] = question }, ct);

    public Task<string> ExtractPidTagsAsync(string text, string sourceType, CancellationToken ct = default) =>
        CallToolAsync("extract_pid_tags", new Dictionary<string, object>
        {
            ["text"] = text,
            ["source_type"] = sourceType
        }, ct);

    public Task<string> MatchPidTagsAsync(IEnumerable<string> pidTags, IEnumerable<string> experionTags, CancellationToken ct = default) =>
        CallToolAsync("match_pid_tags", new Dictionary<string, object>
        {
            ["pid_tags"] = pidTags.ToList(),
            ["experion_tags"] = experionTags.ToList()
        }, ct);

    public Task<string> ParsePidDxfAsync(string filepath, CancellationToken ct = default) =>
        CallToolAsync("parse_pid_dxf", new Dictionary<string, object> { ["filepath"] = filepath }, ct);

    public Task<string> ParsePidPdfAsync(string filepath, bool useOcr = true, CancellationToken ct = default) =>
        CallToolAsync("parse_pid_pdf", new Dictionary<string, object>
        {
            ["filepath"] = filepath,
            ["use_ocr"] = useOcr
        }, ct);

    public Task<string> ParsePidDrawingAsync(string filepath, CancellationToken ct = default) =>
        CallToolAsync("parse_pid_drawing", new Dictionary<string, object> { ["filepath"] = filepath }, ct);

    private async Task<McpResponse?> SendRequestAsync(object request, TimeSpan softTimeout, CancellationToken outerCt)
    {
        // 외부 취소 + 소프트 타임아웃을 묶어서 SendAsync에 전달.
        // HttpClient.Timeout(하드 상한)은 Program.cs에서 더 크게 설정 → 항상 이 CTS가 먼저 발화.
        using var cts = CancellationTokenSource.CreateLinkedTokenSource(outerCt);
        cts.CancelAfter(softTimeout);

        var json = JsonSerializer.Serialize(request);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/mcp")
        {
            Content = content
        };
        // MCP 프로토콜: streamable-http 전송에는 application/json Accept 헤더 필요
        httpRequest.Headers.Add("Accept", "application/json");
        httpRequest.Headers.Add("mcp-protocol-version", "2025-03-26");

        var response = await _httpClient.SendAsync(httpRequest, cts.Token);
        if (!response.IsSuccessStatusCode)
            return null;

        var body = await response.Content.ReadAsStringAsync(cts.Token);
        return JsonSerializer.Deserialize<McpResponse>(body, _jsonOptions);
    }
}

/// <summary>
/// 소프트 타임아웃 초과 시 호출자(UI)에게 알리는 메타데이터.
/// </summary>
public record TimeoutExtendInfo(string ToolName, int SoftTimeoutSec, int ExtendedTimeoutSec);

#region MCP JSON-RPC 모델

public class McpResponse
{
    public string? jsonrpc { get; set; }
    public string? id { get; set; }
    public McpErrorBody? error { get; set; }
    public McpResult? result { get; set; }
}

public class McpErrorBody
{
    public int? code { get; set; }
    public string? message { get; set; }
    public override string ToString() => message ?? "(오류 메시지 없음)";
}

public class McpResult
{
    public McpTool[]? tools { get; set; }
    public McpContentItem[]? content { get; set; }
}

public class McpTool
{
    [JsonPropertyName("name")]
    public string Name { get; set; } = string.Empty;

    [JsonPropertyName("description")]
    public string Description { get; set; } = string.Empty;

    [JsonPropertyName("inputSchema")]
    public JsonElement? InputSchema { get; set; }
}

public class McpContentItem
{
    public string type { get; set; } = string.Empty;
    public string? text { get; set; }
    public string? data { get; set; }
    public string? mimeType { get; set; }
}

#endregion
