using Hc900Crawler.Infrastructure.Mcp;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;

namespace Hc900Crawler.Web.Controllers;

[ApiController]
[Route("api/ollama")]
public class OllamaController : ControllerBase
{
    private readonly HttpClient _httpClient;
    private readonly HttpClient _vllmClient;
    private readonly IConfiguration _config;
    private readonly ILogger<OllamaController> _logger;
    private readonly McpClient _mcpClient;
    private readonly IMemoryCache _memoryCache;

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

    private static readonly SemaphoreSlim _modelCapSemaphore = new(3);

    public OllamaController(
        IHttpClientFactory httpClientFactory,
        IConfiguration config,
        ILogger<OllamaController> logger,
        McpClient mcpClient,
        IMemoryCache memoryCache)
    {
        _httpClient = httpClientFactory.CreateClient("Ollama");
        _vllmClient = httpClientFactory.CreateClient("Vllm");
        _config = config;
        _logger = logger;
        _mcpClient = mcpClient;
        _memoryCache = memoryCache;
    }

    string OllamaConfigPath
    {
        get
        {
            var mcpDir = _config["McpServer:WorkingDirectory"] ?? "../../mcp-server";
            if (!Path.IsPathRooted(mcpDir))
                mcpDir = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), mcpDir));
            return Path.Combine(mcpDir, "ollama-config.json");
        }
    }

    OllamaConfig LoadConfig()
    {
        try
        {
            var path = OllamaConfigPath;
            if (System.IO.File.Exists(path))
            {
                var json = System.IO.File.ReadAllText(path);
                return JsonSerializer.Deserialize<OllamaConfig>(json, _configJsonOptions) ?? new OllamaConfig();
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "[OllamaController] 설정 로드 실패, 기본값 사용");
        }
        return new OllamaConfig();
    }

    string PlantContextPath
    {
        get
        {
            var dir = _config["PromptsDirectory"] ?? "../../prompts";
            if (!Path.IsPathRooted(dir))
                dir = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), dir));
            return Path.Combine(dir, "plant_context.md");
        }
    }

    // plant_context.md 캐시 — 파일 mtime이 바뀌면 자동 재로드
    private static string _plantContextCached = "";
    private static DateTime _plantContextMtime = DateTime.MinValue;
    private static readonly object _plantContextLock = new();

    string LoadPlantContext()
    {
        try
        {
            var p = PlantContextPath;
            if (!System.IO.File.Exists(p)) return "";

            var mtime = System.IO.File.GetLastWriteTimeUtc(p);
            lock (_plantContextLock)
            {
                if (mtime != _plantContextMtime)
                {
                    _plantContextCached = System.IO.File.ReadAllText(p).Trim();
                    _plantContextMtime = mtime;
                }
                return _plantContextCached;
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "[OllamaController] plant_context.md 로드 실패");
            return "";
        }
    }

    private const string BaseSystemPromptKo =
        "당신은 ExperionCrawler 시스템의 운전 보조 AI입니다.\n" +
        "응답은 반드시 한국어로만 작성합니다. 중국어 한자(漢字)·일본어·베트남어 등 다른 언어를 단 한 글자도 섞지 마세요.\n" +
        "특히 '请', '您', '是', '了', '我', '会', '什么', '可以', '需要' 같은 중국어 글자가 응답에 나타나면 잘못된 것입니다 — 한국어로 다시 쓰세요.\n" +
        "영어는 기술 식별자(태그명, SQL 키워드, 도구명, 프로젝트명)에만 한정해서 사용합니다.\n" +
        "한국어 어휘에 한자어가 필요하면 한글로만 표기합니다 (예: '正常' → '정상', '事件' → '이벤트' 또는 '사건').\n" +
        "추측하지 말고, 확실치 않으면 도구로 확인합니다.\n" +
        "수치/시각은 그대로 인용하고, 표/시계열 결과는 가능한 한 표 형식으로 정리해 보여줍니다.";

    private const string AgentModeGuideKo =
        "\n\n## 에이전트 모드 (자율 계획 + 다단계 도구 호출)\n" +
        "복합 질문(예: \"공정 상황 분석\", \"문제점 정리\", \"교대 보고\")은 한 번의 도구 호출로 끝내지 말고\n" +
        "다음 사이클을 반복하세요 (최대 10라운드):\n" +
        "  1) Thought: 지금 무엇을 모르는가? 다음에 어떤 정보가 필요한가?\n" +
        "  2) Action: 가장 적절한 도구를 한 번 호출 (예: active_alarms → query_events → find_tags → query_pv_history)\n" +
        "  3) Observation: 결과를 짧게 해석 (수치/태그명 인용)\n" +
        "  4) 정보가 충분해질 때까지 1~3 반복\n" +
        "  5) Final Answer: 모든 단서를 종합한 한국어 운영자용 답변 (필요 시 표/요약)\n" +
        "주의: 도구 결과의 raw JSON을 그대로 사용자에게 노출하지 말고, 핵심만 정리해서 답변하세요.";

    private const string ToolGuideKo =
        "\n\n## 사용 가능한 MCP 도구\n" +
        "- run_sql:                PostgreSQL SELECT/WITH 실행 (자동 LIMIT 1000 / 30s timeout)\n" +
        "- query_pv_history:       태그 PV 이력 조회 (history_table, recorded_at)\n" +
        "- get_tag_metadata:       태그명 패턴 매칭 검색 (realtime_table)\n" +
        "- find_tags:              태그 통합 검색 — base_tag/설명/area/sub_area (query 생략 시 sub_area 전체 조회)\n" +
        "- list_drawings:          P&ID 도면 목록 (node_map_master)\n" +
        "- query_with_nl:          자연어 → SQL 변환 후 실행\n" +
        "- query_events:           이벤트 히스토리 조회 (event_history_table, tag/event_type/area/기간)\n" +
        "- active_alarms:          현재 활성 알람 (각 태그 최신 이벤트가 ALARM/TRIP인 것)\n" +
        "- summarize_events:       최근 이벤트 LLM 요약 (통계 + 한국어 6~10줄)\n" +
        "- generate_status_report: 운전 상태 종합 보고서 (활성알람+최근이벤트+권고)\n" +
        "- search_kb:              사내 KB(지식베이스) 검색 (system_instrument/plant_operation/procedure/report/vendor_doc)\n" +
        "- classify_intent:        질문 의도 분류 → 추천 도구명 반환 (실행하지 않음, 라우팅 보조)\n" +
        "사용자가 태그 값/이력/DB 정보를 물으면 알맞은 도구를 function calling으로 호출하세요.\n" +
        "도구 결과의 JSON은 그대로 노출하지 말고, 사람이 읽기 쉬운 표/요약으로 변환합니다.\n" +
        "DB 시계열 컬럼은 history_table.recorded_at 이며, time_bucket() 대신 date_trunc 또는 to_timestamp(FLOOR(EPOCH/N*60)*N*60) 공식을 사용합니다.\n" +
        "event_type 값은 ALARM/TRIP/NORMAL/RUN/CHANGE 5종입니다.";

    private async Task EmitToolStart(string toolCallId, string name, string argsJson)
    {
        try
        {
            object argsObj;
            try { argsObj = JsonSerializer.Deserialize<object>(argsJson) ?? new { }; }
            catch { argsObj = argsJson; }
            var data = JsonSerializer.Serialize(new { id = toolCallId, name, args = argsObj });
            await Response.WriteAsync($"event: tool_start\ndata: {data}\n\n");
            await Response.Body.FlushAsync();
        }
        catch { }
    }

    private async Task EmitToolExtending(string toolCallId, string name, int softSec, int extSec)
    {
        try
        {
            var data = JsonSerializer.Serialize(new
            {
                id = toolCallId,
                name,
                softTimeoutSec = softSec,
                extendedTimeoutSec = extSec
            });
            await Response.WriteAsync($"event: tool_extending\ndata: {data}\n\n");
            await Response.Body.FlushAsync();
        }
        catch { }
    }

    private async Task EmitToolResult(string toolCallId, string name, bool ok, string payload)
    {
        try
        {
            const int previewMax = 600;
            var preview = payload.Length > previewMax ? payload.Substring(0, previewMax) + "…" : payload;
            var data = JsonSerializer.Serialize(new
            {
                id = toolCallId,
                name,
                ok,
                preview,
                length = payload.Length,
                payload  // 전체 JSON 그대로(테이블/시계열 자동 렌더에 사용)
            });
            await Response.WriteAsync($"event: tool_result\ndata: {data}\n\n");
            await Response.Body.FlushAsync();
        }
        catch { }
    }

    // KST(=UTC+9) 기준 현재 시각(시:분까지)을 시스템 프롬프트에 매 요청마다 주입.
    // 작은 모델(7B 등)은 'NOW - 30분' 산수가 약하므로, 상대 시간 → UTC 절대 시각 변환 예시를
    // 미리 계산해서 표로 제공한다. 학습 컷오프 연도 추정·미래 시각 호출 차단 목적.
    private static string BuildDateContextKo()
    {
        var utc = DateTimeOffset.UtcNow;
        var kst = utc.ToOffset(TimeSpan.FromHours(9));
        var dow = kst.DayOfWeek switch
        {
            DayOfWeek.Monday => "월", DayOfWeek.Tuesday => "화", DayOfWeek.Wednesday => "수",
            DayOfWeek.Thursday => "목", DayOfWeek.Friday => "금", DayOfWeek.Saturday => "토",
            _ => "일",
        };

        // 상대 시간 변환 표를 미리 계산 — LLM이 산수 없이 그대로 인용 가능.
        // 분 단위로 끊어서 일관성 확보 (UTC 분 단위, 초/밀리초는 사용자가 안 따짐).
        var nowMin = new DateTimeOffset(utc.Year, utc.Month, utc.Day, utc.Hour, utc.Minute, 0, TimeSpan.Zero);
        string IsoZ(DateTimeOffset t) => t.ToString("yyyy-MM-ddTHH:mm:ssZ");

        var last30Start = nowMin.AddMinutes(-30);
        var last1hStart = nowMin.AddHours(-1);
        var last24hStart = nowMin.AddHours(-24);
        // KST 자정 = UTC 전날 15:00
        var kstMidnightToday = new DateTimeOffset(kst.Year, kst.Month, kst.Day, 0, 0, 0, TimeSpan.FromHours(9));
        var kstMidnightTodayUtc = kstMidnightToday.ToUniversalTime();
        var kstMidnightTomorrowUtc = kstMidnightToday.AddDays(1).ToUniversalTime();
        var kstMidnightYesterdayUtc = kstMidnightToday.AddDays(-1).ToUniversalTime();

        return
            $"\n\n## 현재 시각 (도구 호출 시 반드시 참조)\n" +
            $"지금: {kst:yyyy-MM-dd HH:mm} ({dow}) KST = {utc:yyyy-MM-dd HH:mm} UTC\n" +
            $"DB의 모든 시각(recorded_at, event_time, since, until 등)은 **UTC 기준**으로 저장·전달합니다.\n\n" +
            $"### 상대 시간 → UTC 절대 시각 변환 (그대로 인용해 도구 인자로 사용)\n" +
            $"- \"지난 30분\"   → since: {IsoZ(last30Start)}  until: {IsoZ(nowMin)}\n" +
            $"- \"지난 1시간\"   → since: {IsoZ(last1hStart)}  until: {IsoZ(nowMin)}\n" +
            $"- \"지난 24시간\"  → since: {IsoZ(last24hStart)}  until: {IsoZ(nowMin)}\n" +
            $"- \"오늘\"(KST)    → since: {IsoZ(kstMidnightTodayUtc)}  until: {IsoZ(kstMidnightTomorrowUtc)}\n" +
            $"- \"어제\"(KST)    → since: {IsoZ(kstMidnightYesterdayUtc)}  until: {IsoZ(kstMidnightTodayUtc)}\n\n" +
            $"### 절대 금지 사항\n" +
            $"- {IsoZ(nowMin)} **이후 시각(미래)** 을 since/until/time_from/time_to 등으로 사용 금지.\n" +
            $"- KST 시각에 'Z' 표기 직접 붙이지 마세요(예: 'KST 14:00' → '14:00Z'는 틀림). 반드시 −9시간 변환 후 'Z'.\n" +
            $"- 학습 시점의 연도(2023, 2024 등) 사용 금지. 현재 연도는 **{kst.Year}년**.\n" +
            $"- 위 표에 없는 상대 시간(예: \"지난 2시간\")은 \"지난 1시간\" 패턴을 참고해 직접 계산하되, 결과가 {IsoZ(nowMin)}을 초과하면 안 됩니다.";
    }

    string ComposeSystemPrompt(string? userPrompt, bool toolsEnabled, bool agentMode = false)
    {
        var sb = new StringBuilder();
        sb.Append(BaseSystemPromptKo);
        sb.Append(BuildDateContextKo());

        var plant = LoadPlantContext();
        if (!string.IsNullOrWhiteSpace(plant))
        {
            sb.Append("\n\n## 플랜트 컨텍스트\n");
            sb.Append(plant);
        }

        if (toolsEnabled)
        {
            sb.Append(ToolGuideKo);
            if (agentMode) sb.Append(AgentModeGuideKo);
        }

        if (!string.IsNullOrWhiteSpace(userPrompt))
        {
            sb.Append("\n\n## 사용자 추가 지침\n");
            sb.Append(userPrompt.Trim());
        }

        return sb.ToString();
    }

    [HttpGet("models")]
    public async Task<IActionResult> GetModels()
    {
        try
        {
            var cfg = LoadConfig();
            var res = await _httpClient.GetAsync($"{cfg.BaseUrl}/api/tags");
            if (!res.IsSuccessStatusCode)
                return Ok(new { success = false, error = $"Ollama HTTP {(int)res.StatusCode}", models = Array.Empty<string>() });

            var body = await res.Content.ReadAsStringAsync();
            var doc = JsonDocument.Parse(body);
            var allModels = new List<string>();
            if (doc.RootElement.TryGetProperty("models", out var arr))
            {
                foreach (var m in arr.EnumerateArray())
                {
                    if (m.TryGetProperty("name", out var name))
                    {
                        var n = name.GetString();
                        if (!string.IsNullOrEmpty(n)) allModels.Add(n);
                    }
                }
            }

            // 임베딩 전용 모델(bge-m3, nomic-embed-text 등)을 채팅 셀렉터에서 제거.
            // Ollama /api/show 의 capabilities에 'completion' 이 있어야 채팅 가능.
            // 병렬 조회로 모델 수가 많아도 지연을 최소화.
            var chatModels = new List<string>();
            var embeddingModels = new List<string>();
            var tasks = allModels.Select(async n =>
            {
                await _modelCapSemaphore.WaitAsync();
                try
                {
                    var caps = await GetModelCapabilitiesAsync(cfg.BaseUrl, n);
                    return (name: n, isChat: caps.Contains("completion"));
                }
                finally { _modelCapSemaphore.Release(); }
            }).ToList();
            var results = await Task.WhenAll(tasks);
            foreach (var (n, isChat) in results)
            {
                if (isChat) chatModels.Add(n);
                else embeddingModels.Add(n);
            }

            return Ok(new
            {
                success = true,
                models = chatModels.OrderBy(x => x).ToArray(),
                // 진단/안내용 — UI에서 셀렉터 옆에 회색 안내로 노출 가능
                excluded = embeddingModels.OrderBy(x => x).ToArray()
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[OllamaController] 모델 조회 실패");
            return Ok(new { success = false, error = ex.Message, models = Array.Empty<string>() });
        }
    }

    // capabilities 캐시 — /api/show는 비교적 무거우므로 메모리 캐시(5분).
    // IMemoryCache 사용으로 TTL 만료 항목 자동 정리.
    private async Task<string[]> GetModelCapabilitiesAsync(string baseUrl, string model)
    {
        var cacheKey = $"caps_{model}";
        if (_memoryCache.TryGetValue(cacheKey, out string[]? cached))
            return cached ?? Array.Empty<string>();

        try
        {
            using var req = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/api/show")
            {
                Content = new StringContent(JsonSerializer.Serialize(new { name = model }), Encoding.UTF8, "application/json")
            };
            var r = await _httpClient.SendAsync(req);
            if (!r.IsSuccessStatusCode) return Array.Empty<string>();
            var d = JsonDocument.Parse(await r.Content.ReadAsStringAsync());
            string[] caps;
            if (d.RootElement.TryGetProperty("capabilities", out var capsEl) && capsEl.ValueKind == JsonValueKind.Array)
                caps = capsEl.EnumerateArray().Select(c => c.GetString() ?? "").Where(s => s.Length > 0).ToArray();
            else
                caps = Array.Empty<string>();
            _memoryCache.Set(cacheKey, caps, TimeSpan.FromMinutes(5));
            return caps;
        }
        catch { return Array.Empty<string>(); }
    }

    [HttpPost("chat")]
    public async Task<IActionResult> Chat([FromBody] OllamaChatRequest req)
    {
        try
        {
            var cfg = LoadConfig();
            var payload = new
            {
                model = req.Model,
                messages = req.Messages,
                system = ComposeSystemPrompt(req.SystemPrompt, toolsEnabled: false),
                stream = false
            };
            var content = new StringContent(
                JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");

            var res = await _httpClient.PostAsync($"{cfg.BaseUrl}/api/chat", content);
            if (!res.IsSuccessStatusCode)
            {
                var err = await res.Content.ReadAsStringAsync();
                return Ok(new { success = false, error = err });
            }

            var body = await res.Content.ReadAsStringAsync();
            var doc = JsonDocument.Parse(body);
            string? reply = null;
            if (doc.RootElement.TryGetProperty("message", out var msg)
                && msg.TryGetProperty("content", out var cnt))
            {
                reply = cnt.GetString();
            }
            return Ok(new { success = true, reply });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[OllamaController] 채팅 실패");
            return Ok(new { success = false, error = ex.Message, reply = (string?)null });
        }
    }

    [HttpPost("chat/stream")]
    public async Task OllamaChatStream([FromBody] OllamaChatRequest req)
    {
        Response.Headers["Content-Type"] = "text/event-stream";
        Response.Headers["Cache-Control"] = "no-cache";
        Response.Headers["Connection"] = "keep-alive";
        Response.Headers["X-Accel-Buffering"] = "no";

        try
        {
            var cfg = LoadConfig();
            var payload = new
            {
                model = req.Model,
                messages = req.Messages,
                system = ComposeSystemPrompt(req.SystemPrompt, toolsEnabled: false),
                stream = true
            };
            var content = new StringContent(
                JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");

            using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"{cfg.BaseUrl}/api/chat")
            {
                Content = content
            };

            using var res = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted);

            if (!res.IsSuccessStatusCode)
            {
                await Response.WriteAsync($"event: error\ndata: {res.StatusCode}\n\n");
                await Response.Body.FlushAsync();
                return;
            }

            var stream = await res.Content.ReadAsStreamAsync();
            using var reader = new StreamReader(stream, Encoding.UTF8);
            while (true)
            {
                if (HttpContext.RequestAborted.IsCancellationRequested) break;
                var line = await reader.ReadLineAsync();
                if (line == null) break;
                if (string.IsNullOrWhiteSpace(line)) continue;

                try
                {
                    var doc = JsonDocument.Parse(line);
                    await Response.WriteAsync($"event: message\ndata: {line}\n\n");
                    await Response.Body.FlushAsync();

                    if (doc.RootElement.TryGetProperty("done", out var done) && done.GetBoolean())
                        break;
                }
                catch
                {
                }
            }

            await Response.WriteAsync("event: done\ndata: {}\n\n");
            await Response.Body.FlushAsync();
        }
        catch (OperationCanceledException)
        {
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[OllamaController] 스트리밍 오류");
            try
            {
                await Response.WriteAsync($"event: error\ndata: {JsonSerializer.Serialize(new { message = ex.Message })}\n\n");
                await Response.Body.FlushAsync();
            }
            catch { }
        }
    }

    [HttpGet("config")]
    public IActionResult GetConfig()
    {
        var cfg = LoadConfig();
        return Ok(new { success = true, host = cfg.Host, port = cfg.Port, baseUrl = cfg.BaseUrl });
    }

    [HttpPost("config")]
    public IActionResult SetConfig([FromBody] OllamaConfig cfg)
    {
        try
        {
            var path = OllamaConfigPath;
            var dir = Path.GetDirectoryName(path);
            if (dir != null && !System.IO.Directory.Exists(dir))
                System.IO.Directory.CreateDirectory(dir);

            var json = JsonSerializer.Serialize(new
            {
                host = cfg.Host,
                port = cfg.Port
            }, new JsonSerializerOptions { WriteIndented = true });
            System.IO.File.WriteAllText(path, json);
            _logger.LogInformation("[OllamaController] 설정 저장: {Host}:{Port}", cfg.Host, cfg.Port);
            return Ok(new { success = true, host = cfg.Host, port = cfg.Port, baseUrl = cfg.BaseUrl });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[OllamaController] 설정 저장 실패");
            return Ok(new { success = false, error = ex.Message });
        }
    }

    [HttpGet("ping")]
    public async Task<IActionResult> Ping()
    {
        try
        {
            var cfg = LoadConfig();
            var res = await _httpClient.GetAsync($"{cfg.BaseUrl}/api/tags");
            return Ok(new { success = res.IsSuccessStatusCode, host = cfg.Host, port = cfg.Port });
        }
        catch (Exception ex)
        {
            return Ok(new { success = false, error = ex.Message });
        }
    }

    // ── vLLM (OpenAI-compatible API) ─────────────────────────────────────

    string VllmModelPath
    {
        get
        {
            var mcpDir = _config["McpServer:WorkingDirectory"] ?? "../../mcp-server";
            if (!Path.IsPathRooted(mcpDir))
                mcpDir = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), mcpDir));
            return Path.Combine(mcpDir, "llm-model.json");
        }
    }

    string LoadVllmModel()
    {
        try
        {
            var path = VllmModelPath;
            if (System.IO.File.Exists(path))
            {
                var json = System.IO.File.ReadAllText(path);
                var doc = JsonDocument.Parse(json);
                return doc.RootElement.TryGetProperty("vllm_model", out var v) ? v.GetString() ?? "Qwen3.6-27B-FP8" : "Qwen3.6-27B-FP8";
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "[OllamaController] vLLM 모델 로드 실패");
        }
        return "Qwen3.6-27B-FP8";
    }

    [HttpGet("vllm/models")]
    public async Task<IActionResult> GetVllmModels()
    {
        try
        {
            var res = await _vllmClient.GetAsync("/v1/models");
            if (!res.IsSuccessStatusCode)
                return Ok(new { success = false, error = $"vLLM HTTP {(int)res.StatusCode}", models = Array.Empty<string>() });

            var body = await res.Content.ReadAsStringAsync();
            var doc = JsonDocument.Parse(body);
            var models = new List<string>();
            if (doc.RootElement.TryGetProperty("data", out var arr))
            {
                foreach (var m in arr.EnumerateArray())
                {
                    if (m.TryGetProperty("id", out var name))
                        models.Add(name.GetString() ?? "");
                }
            }
            return Ok(new { success = true, models = models.OrderBy(x => x).ToArray() });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[OllamaController] vLLM 모델 조회 실패");
            return Ok(new { success = false, error = ex.Message, models = Array.Empty<string>() });
        }
    }

    [HttpPost("vllm/chat")]
    public async Task<IActionResult> VllmChat([FromBody] OllamaChatRequest req)
    {
        try
        {
            var model = req.Model;
            var msgList = new List<object>();
            var sysPrompt = ComposeSystemPrompt(req.SystemPrompt, toolsEnabled: false);
            if (!string.IsNullOrEmpty(sysPrompt))
                msgList.Add(new { role = "system", content = sysPrompt });
            foreach (var m in req.Messages)
                msgList.Add(m);
            var payload = new
            {
                model,
                messages = msgList,
                stream = false
            };
            var content = new StringContent(
                JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");

            var res = await _vllmClient.PostAsync("/v1/chat/completions", content);
            if (!res.IsSuccessStatusCode)
            {
                var err = await res.Content.ReadAsStringAsync();
                return Ok(new { success = false, error = err });
            }

            var body = await res.Content.ReadAsStringAsync();
            var doc = JsonDocument.Parse(body);
            string? reply = null;
            if (doc.RootElement.TryGetProperty("choices", out var choices) && choices.GetArrayLength() > 0)
            {
                var first = choices[0];
                if (first.TryGetProperty("message", out var msg) && msg.TryGetProperty("content", out var cnt))
                    reply = cnt.GetString();
            }
            return Ok(new { success = true, reply });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[OllamaController] vLLM 채팅 실패");
            return Ok(new { success = false, error = ex.Message, reply = (string?)null });
        }
    }

    // Phase 7.2: 대화 요약 — 오래된 messages 배열을 한국어 5~8줄로 압축
    [HttpPost("summarize")]
    public async Task<IActionResult> Summarize([FromBody] OllamaSummarizeRequest req)
    {
        try
        {
            if (req?.Messages == null || req.Messages.Length == 0)
                return Ok(new { success = false, error = "messages required", summary = "" });

            // 대화를 평문으로 직렬화
            var convo = new StringBuilder();
            foreach (var m in req.Messages)
            {
                if (string.IsNullOrWhiteSpace(m.Content)) continue;
                var role = m.Role == "user" ? "사용자" : (m.Role == "assistant" ? "어시스턴트" : m.Role);
                convo.Append('[').Append(role).Append("] ").AppendLine(m.Content.Trim());
            }
            var convoText = convo.ToString();
            if (convoText.Length > 12000) convoText = convoText.Substring(convoText.Length - 12000);

            var sysMsg = new
            {
                role = "system",
                content =
                    "당신은 대화 요약 전문가입니다.\n" +
                    "아래 사용자–어시스턴트 대화를 한국어 5~8줄로 압축하세요.\n" +
                    "- 사용자가 무엇을 물었는지, 어떤 결정/답변이 있었는지 핵심만\n" +
                    "- 도구 이름·태그명·시각·수치 등 식별자는 그대로 인용\n" +
                    "- 불필요한 인사말/장식 문구 제외\n" +
                    "결과는 요약문만 반환 (서두/말미 부가 설명 금지)."
            };
            var usrMsg = new { role = "user", content = convoText };

            var model = string.IsNullOrWhiteSpace(req.Model) ? LoadVllmModel() : req.Model;
            var payload = new
            {
                model,
                messages = new object[] { sysMsg, usrMsg },
                stream = false,
                max_tokens = 600,
                temperature = 0.2
            };

            var content = new StringContent(
                JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
            var res = await _vllmClient.PostAsync("/v1/chat/completions", content);
            if (!res.IsSuccessStatusCode)
            {
                var err = await res.Content.ReadAsStringAsync();
                return Ok(new { success = false, error = err, summary = "" });
            }
            var body = await res.Content.ReadAsStringAsync();
            var doc = JsonDocument.Parse(body);
            string summary = "";
            if (doc.RootElement.TryGetProperty("choices", out var choices) && choices.GetArrayLength() > 0)
            {
                var first = choices[0];
                if (first.TryGetProperty("message", out var msg) && msg.TryGetProperty("content", out var cnt))
                    summary = cnt.GetString() ?? "";
            }
            return Ok(new { success = true, summary = summary.Trim() });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[OllamaController] 대화 요약 실패");
            return Ok(new { success = false, error = ex.Message, summary = "" });
        }
    }

    [HttpPost("vllm/chat/stream")]
    public async Task VllmChatStream([FromBody] OllamaChatRequest req)
    {
        Response.Headers["Content-Type"] = "text/event-stream";
        Response.Headers["Cache-Control"] = "no-cache";
        Response.Headers["Connection"] = "keep-alive";
        Response.Headers["X-Accel-Buffering"] = "no";

        try
        {
            var hasTools = req.Tools != null && req.Tools.Length > 0;
            if (hasTools)
            {
                await VllmChatStreamWithTools(req);
            }
            else
            {
                await VllmChatStreamSimple(req);
            }
        }
        catch (OperationCanceledException)
        {
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[OllamaController] vLLM 스트리밍 오류");
            try
            {
                await Response.WriteAsync($"event: error\ndata: {JsonSerializer.Serialize(new { message = ex.Message })}\n\n");
                await Response.Body.FlushAsync();
            }
            catch { }
        }
    }

    private async Task VllmChatStreamSimple(OllamaChatRequest req)
    {
        var msgList = new List<object>();
        var sysPrompt = ComposeSystemPrompt(req.SystemPrompt, toolsEnabled: false);
        if (!string.IsNullOrEmpty(sysPrompt))
            msgList.Add(new { role = "system", content = sysPrompt });
        foreach (var m in req.Messages)
            msgList.Add(m);
        var payload = new
        {
            model = req.Model,
            messages = msgList,
            stream = true
        };
        var content = new StringContent(
            JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");

        using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/v1/chat/completions")
        {
            Content = content
        };

        using var res = await _vllmClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted);

        if (!res.IsSuccessStatusCode)
        {
            await Response.WriteAsync($"event: error\ndata: {res.StatusCode}\n\n");
            await Response.Body.FlushAsync();
            return;
        }

        var stream = await res.Content.ReadAsStreamAsync();
        using var reader = new StreamReader(stream, Encoding.UTF8);
        while (true)
        {
            if (HttpContext.RequestAborted.IsCancellationRequested) break;
            var line = await reader.ReadLineAsync();
            if (line == null) break;

            if (line.StartsWith("data: ") == false) continue;
            var data = line.Substring(6);
            if (string.IsNullOrWhiteSpace(data) || data == "[DONE]") continue;

            try
            {
                var doc = JsonDocument.Parse(data);
                if (doc.RootElement.TryGetProperty("choices", out var choices) && choices.GetArrayLength() > 0)
                {
                    var delta = choices[0];
                    if (delta.TryGetProperty("delta", out var d) && d.TryGetProperty("content", out var c))
                    {
                        var text = c.GetString() ?? "";
                        if (!string.IsNullOrEmpty(text))
                        {
                            var msgJson = JsonSerializer.Serialize(new { message = new { content = text } });
                            await Response.WriteAsync($"event: message\ndata: {msgJson}\n\n");
                            await Response.Body.FlushAsync();
                        }
                    }
                }
            }
            catch
            {
            }
        }

        await Response.WriteAsync("event: done\ndata: {}\n\n");
        await Response.Body.FlushAsync();
    }

    private async Task VllmChatStreamWithTools(OllamaChatRequest req)
    {
        var messages = new List<object>();
        var sysPrompt = ComposeSystemPrompt(req.SystemPrompt, toolsEnabled: true, agentMode: req.AgentMode);
        if (!string.IsNullOrEmpty(sysPrompt))
            messages.Add(new { role = "system", content = sysPrompt });
        foreach (var m in req.Messages)
            messages.Add(m);

        int maxToolRounds = 10;
        int toolRound = 0;

        while (toolRound < maxToolRounds)
        {
            toolRound++;

            var nonStreamPayload = new
            {
                model = req.Model,
                messages,
                tools = req.Tools,
                tool_choice = "auto",
                stream = false
            };

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

            using var httpReq = new HttpRequestMessage(HttpMethod.Post, "/v1/chat/completions")
            {
                Content = reqContent
            };

            using var res = await _vllmClient.SendAsync(httpReq, HttpContext.RequestAborted);
            if (!res.IsSuccessStatusCode)
            {
                var err = await res.Content.ReadAsStringAsync();
                await Response.WriteAsync($"event: error\ndata: {err}\n\n");
                await Response.Body.FlushAsync();
                return;
            }

            var body = await res.Content.ReadAsStringAsync();
            using var doc = JsonDocument.Parse(body);

            JsonElement choice;
            if (!doc.RootElement.TryGetProperty("choices", out var choices) || choices.GetArrayLength() == 0)
            {
                await Response.WriteAsync("event: error\ndata: No choices in response\n\n");
                await Response.Body.FlushAsync();
                return;
            }
            choice = choices[0];

            var finishReason = choice.TryGetProperty("finish_reason", out var fr) ? fr.GetString() : "stop";

            if (finishReason == "tool_calls")
            {
                var msg = choice.GetProperty("message");

                var tcList = new List<object>();
                var toolCallInfos = new List<(string id, string name, string args)>();
                if (msg.TryGetProperty("tool_calls", out var toolCalls))
                {
                    foreach (var tc in toolCalls.EnumerateArray())
                    {
                        var tcId = tc.GetProperty("id").GetString() ?? $"tc_{toolRound}_{Guid.NewGuid():N}";
                        var func = tc.GetProperty("function");
                        var funcName = func.GetProperty("name").GetString() ?? "";
                        var funcArgs = func.GetProperty("arguments").GetString() ?? "{}";

                        tcList.Add(new
                        {
                            id = tcId,
                            type = "function",
                            function = new
                            {
                                name = funcName,
                                arguments = funcArgs
                            }
                        });
                        toolCallInfos.Add((tcId, funcName, funcArgs));
                    }
                }

                messages.Add(new
                {
                    role = "assistant",
                    content = (string?)null,
                    tool_calls = tcList
                });

                foreach (var (tcId, funcName, funcArgs) in toolCallInfos)
                {
                    await EmitToolStart(tcId, funcName, funcArgs);

                    try
                    {
                        var args = JsonSerializer.Deserialize<Dictionary<string, object>>(funcArgs)
                            ?? new Dictionary<string, object>();

                        var toolResult = await _mcpClient.CallToolAsync(
                            funcName, args, HttpContext.RequestAborted,
                            onTimeoutExtended: info => EmitToolExtending(tcId, info.ToolName, info.SoftTimeoutSec, info.ExtendedTimeoutSec));

                        await EmitToolResult(tcId, funcName, ok: true, payload: toolResult);

                        messages.Add(new
                        {
                            role = "tool",
                            tool_call_id = tcId,
                            content = toolResult
                        });
                    }
                    catch (Exception ex)
                    {
                        await EmitToolResult(tcId, funcName, ok: false, payload: ex.Message);
                        messages.Add(new
                        {
                            role = "tool",
                            tool_call_id = tcId,
                            content = $"도구 실행 오류: {ex.Message}"
                        });
                    }

                    if (HttpContext.RequestAborted.IsCancellationRequested) return;
                }

                continue;
            }

            // content에서 JSON 형식 텍스트 도구 호출 감지 (tool_calls API 대신 JSON 텍스트를 출력하는 모델 처리)
            var stopContent = "";
            if (choice.TryGetProperty("message", out var stopMsgEl) && stopMsgEl.TryGetProperty("content", out var stopCntEl))
                stopContent = stopCntEl.GetString() ?? "";

            // 배열 포맷 우선 시도: [{tool, params}, ...] — 모델이 여러 도구를 배열로 출력할 때
            var jsonArrayCandidate = ExtractFirstJsonArray(stopContent);
            var jsonObjectCandidates = new List<string>();
            if (!string.IsNullOrEmpty(jsonArrayCandidate))
            {
                try
                {
                    using var arrDoc = JsonDocument.Parse(jsonArrayCandidate);
                    if (arrDoc.RootElement.ValueKind == JsonValueKind.Array)
                        foreach (var elem in arrDoc.RootElement.EnumerateArray())
                            if (elem.ValueKind == JsonValueKind.Object)
                                jsonObjectCandidates.Add(elem.GetRawText());
                }
                catch { }
            }
            // 배열이 없거나 파싱 실패 → 단일 객체 추출
            if (jsonObjectCandidates.Count == 0)
            {
                var single = ExtractFirstJsonObject(stopContent);
                if (!string.IsNullOrEmpty(single)) jsonObjectCandidates.Add(single);
            }

            if (jsonObjectCandidates.Count > 0)
            {
                bool toolExecuted = false;
                var toolResults = new List<string>();
                var toolNames = new List<string>();

                foreach (var jsonCandidate in jsonObjectCandidates)
                {
                    try
                    {
                        using var jsonDoc = JsonDocument.Parse(jsonCandidate);
                        if (jsonDoc.RootElement.ValueKind != JsonValueKind.Object) continue;

                        var propNames = jsonDoc.RootElement.EnumerateObject().Select(p => p.Name).ToHashSet();
                        string? detectedTool = null;
                        var args = new Dictionary<string, object>();

                        // 포맷 1: {"tool"|"tool_name": "...", "parameters"|"arguments"|"params"|"args": {...}}
                        if (propNames.Contains("tool") || propNames.Contains("tool_name"))
                        {
                            var toolKey = propNames.Contains("tool") ? "tool" : "tool_name";
                            detectedTool = jsonDoc.RootElement.GetProperty(toolKey).GetString();
                            // params 키 우선순위: parameters > arguments > params > args
                            var paramsKey = propNames.Contains("parameters") ? "parameters"
                                : propNames.Contains("arguments") ? "arguments"
                                : propNames.Contains("params") ? "params"
                                : propNames.Contains("args") ? "args" : null;
                            if (paramsKey != null && jsonDoc.RootElement.TryGetProperty(paramsKey, out var paramsEl) && paramsEl.ValueKind == JsonValueKind.Object)
                            {
                                foreach (var prop in paramsEl.EnumerateObject())
                                {
                                    var key = (detectedTool == "query_with_nl" && prop.Name == "query") ? "question" : prop.Name;
                                    args[key] = prop.Value.ValueKind == JsonValueKind.String
                                        ? (object)(prop.Value.GetString() ?? "")
                                        : JsonSerializer.Deserialize<object>(prop.Value.GetRawText()) ?? "";
                                }
                            }
                        }
                        // 포맷 2: {"function": "...", "args"|"input": {...}} 또는 {"name": "...", "input"|"args": {...}}
                        else if ((propNames.Contains("function") || propNames.Contains("name")) &&
                                 (propNames.Contains("args") || propNames.Contains("input")))
                        {
                            detectedTool = (propNames.Contains("function")
                                ? jsonDoc.RootElement.GetProperty("function")
                                : jsonDoc.RootElement.GetProperty("name")).GetString();
                            var paramsKey = propNames.Contains("args") ? "args" : "input";
                            if (jsonDoc.RootElement.TryGetProperty(paramsKey, out var paramsEl) && paramsEl.ValueKind == JsonValueKind.Object)
                            {
                                foreach (var prop in paramsEl.EnumerateObject())
                                {
                                    var key = (detectedTool == "query_with_nl" && prop.Name == "query") ? "question" : prop.Name;
                                    args[key] = prop.Value.ValueKind == JsonValueKind.String
                                        ? (object)(prop.Value.GetString() ?? "")
                                        : JsonSerializer.Deserialize<object>(prop.Value.GetRawText()) ?? "";
                                }
                            }
                        }
                        // 포맷 3: 플랫 JSON — 파라미터명으로 도구 추론
                        else if (propNames.Contains("sql")) detectedTool = "run_sql";
                        else if (propNames.Contains("tag_names")) detectedTool = "query_pv_history";
                        else if (propNames.Contains("question")) detectedTool = "query_with_nl";
                        else if (propNames.Contains("query")) detectedTool = "query_with_nl";

                        // 포맷 3일 때 args 채우기
                        if (detectedTool != null && args.Count == 0)
                        {
                            foreach (var prop in jsonDoc.RootElement.EnumerateObject())
                            {
                                var key = (detectedTool == "query_with_nl" && prop.Name == "query") ? "question" : prop.Name;
                                args[key] = prop.Value.ValueKind == JsonValueKind.String
                                    ? (object)(prop.Value.GetString() ?? "")
                                    : JsonSerializer.Deserialize<object>(prop.Value.GetRawText()) ?? "";
                            }
                        }

                        if (detectedTool != null && args.Count > 0)
                        {
                            var pseudoId = $"jsontc_{toolRound}_{Guid.NewGuid():N}";
                            var argsJson = JsonSerializer.Serialize(args);
                            await EmitToolStart(pseudoId, detectedTool, argsJson);
                            try
                            {
                                var toolResult = await _mcpClient.CallToolAsync(
                                    detectedTool, args, HttpContext.RequestAborted,
                                    onTimeoutExtended: info => EmitToolExtending(pseudoId, info.ToolName, info.SoftTimeoutSec, info.ExtendedTimeoutSec));
                                await EmitToolResult(pseudoId, detectedTool, ok: true, payload: toolResult);
                                toolResults.Add($"[{detectedTool} 결과]\n{toolResult}");
                                toolNames.Add(detectedTool);
                                toolExecuted = true;
                            }
                            catch (Exception ex)
                            {
                                await EmitToolResult(pseudoId, detectedTool, ok: false, payload: ex.Message);
                                _logger.LogWarning(ex, "[OllamaController] 텍스트 형식 도구 호출 실패: {Tool}", detectedTool);
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger.LogWarning(ex, "[OllamaController] JSON 도구 감지 파싱 실패: {Candidate}", jsonCandidate);
                    }

                    if (HttpContext.RequestAborted.IsCancellationRequested) return;
                }

                if (toolExecuted)
                {
                    // 모든 도구 결과를 합쳐서 한 번에 LLM에 전달
                    var combinedResults = string.Join("\n\n", toolResults);
                    messages.Add(new { role = "assistant", content = stopContent });
                    messages.Add(new { role = "user", content = $"{combinedResults}\n\n위 결과를 바탕으로 사용자의 질문에 자연어로 답변해주세요." });
                    continue;
                }
            }

            // 첫 번째 비스트리밍 응답의 content를 직접 전달 (두 번째 LLM 호출 없이)
            if (!string.IsNullOrEmpty(stopContent))
            {
                var msgJson = JsonSerializer.Serialize(new { message = new { content = stopContent } });
                await Response.WriteAsync($"event: message\ndata: {msgJson}\n\n");
                await Response.Body.FlushAsync();
                await Response.WriteAsync("event: done\ndata: {}\n\n");
                await Response.Body.FlushAsync();
                return;
            }

            // content가 없는 경우에만 스트리밍 호출로 fallback
            var streamPayload = new
            {
                model = req.Model,
                messages,
                stream = true
            };

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

            using var streamReq = new HttpRequestMessage(HttpMethod.Post, "/v1/chat/completions")
            {
                Content = streamContent
            };

            using var streamRes = await _vllmClient.SendAsync(streamReq, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted);
            if (!streamRes.IsSuccessStatusCode)
            {
                await Response.WriteAsync($"event: error\ndata: {streamRes.StatusCode}\n\n");
                await Response.Body.FlushAsync();
                return;
            }

            var sseStream = await streamRes.Content.ReadAsStreamAsync();
            using var sseReader = new StreamReader(sseStream, Encoding.UTF8);
            while (true)
            {
                if (HttpContext.RequestAborted.IsCancellationRequested) return;
                var line = await sseReader.ReadLineAsync();
                if (line == null) break;

                if (!line.StartsWith("data: ")) continue;
                var data = line.Substring(6);
                if (string.IsNullOrWhiteSpace(data) || data == "[DONE]") continue;

                try
                {
                    var sseDoc = JsonDocument.Parse(data);
                    if (sseDoc.RootElement.TryGetProperty("choices", out var sseChoices) && sseChoices.GetArrayLength() > 0)
                    {
                        var delta = sseChoices[0];
                        if (delta.TryGetProperty("delta", out var d) && d.TryGetProperty("content", out var c))
                        {
                            var text = c.GetString() ?? "";
                            if (!string.IsNullOrEmpty(text))
                            {
                                var msgJson = JsonSerializer.Serialize(new { message = new { content = text } });
                                await Response.WriteAsync($"event: message\ndata: {msgJson}\n\n");
                                await Response.Body.FlushAsync();
                            }
                        }
                    }
                }
                catch { }
            }

            await Response.WriteAsync("event: done\ndata: {}\n\n");
            await Response.Body.FlushAsync();
            return;
        }

        await Response.WriteAsync("event: error\ndata: Too many tool call rounds\n\n");
        await Response.Body.FlushAsync();
    }

    [HttpGet("vllm/ping")]
    public async Task<IActionResult> VllmPing()
    {
        try
        {
            var res = await _vllmClient.GetAsync("/v1/models");
            return Ok(new { success = res.IsSuccessStatusCode, model = LoadVllmModel() });
        }
        catch (Exception ex)
        {
            return Ok(new { success = false, error = ex.Message });
        }
    }

    // content 안에서 첫 번째 완전한 JSON 객체를 추출 (앞뒤 텍스트 무시, 모델 무관)
    private static string ExtractFirstJsonObject(string content)
    {
        var start = content.IndexOf('{');
        if (start < 0) return "";
        return ExtractBalanced(content, start, '{', '}');
    }

    private static string ExtractFirstJsonArray(string content)
    {
        var start = content.IndexOf('[');
        if (start < 0) return "";
        return ExtractBalanced(content, start, '[', ']');
    }

    private static string ExtractBalanced(string content, int start, char open, char close)
    {
        var depth = 0;
        bool inString = false;
        bool escaped = false;
        for (int i = start; i < content.Length; i++)
        {
            char c = content[i];
            if (escaped) { escaped = false; continue; }
            if (c == '\\') { escaped = true; continue; }
            if (c == '"') { inString = !inString; continue; }
            if (!inString)
            {
                if (c == open) depth++;
                else if (c == close) { depth--; if (depth == 0) return content.Substring(start, i - start + 1); }
            }
        }
        return "";
    }
}

public class OllamaConfig
{
    public string Host { get; set; } = "localhost";
    public int Port { get; set; } = 11434;

    public string BaseUrl => $"http://{Host}:{Port}";
}

public class OllamaChatRequest
{
    public string Model { get; set; } = "";
    public OllamaMessage[] Messages { get; set; } = Array.Empty<OllamaMessage>();
    public string? SystemPrompt { get; set; }
    public OllamaTool[]? Tools { get; set; }
    public bool AgentMode { get; set; } = false;
}

public class OllamaSummarizeRequest
{
    public string? Model { get; set; }
    public OllamaMessage[] Messages { get; set; } = Array.Empty<OllamaMessage>();
}

public class OllamaMessage
{
    [JsonPropertyName("role")]
    public string Role { get; set; } = "";
    [JsonPropertyName("content")]
    public string? Content { get; set; }
}

public class OllamaTool
{
    [JsonPropertyName("type")]
    public string Type { get; set; } = "function";

    [JsonPropertyName("function")]
    public OllamaFunction Function { get; set; } = new();
}

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

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

    [JsonPropertyName("parameters")]
    public JsonElement? Parameters { get; set; }
}
