using Hc900Crawler.Core.Domain.Entities;
using Hc900Crawler.Infrastructure.Database;
using Hc900Crawler.Infrastructure.Kb;
using Hc900Crawler.Infrastructure.Mcp;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Hc900Crawler.Web.Controllers;

[ApiController]
[Route("api/kb")]
public class KbController : ControllerBase
{
    private readonly Hc900DbContext _db;
    private readonly KbStorageService _storage;
    private readonly KbQdrantClient _qdrant;
    private readonly IKbAuthService _auth;
    private readonly McpClient _mcp;
    private readonly ILogger<KbController> _logger;

    public KbController(
        Hc900DbContext db,
        KbStorageService storage,
        KbQdrantClient qdrant,
        IKbAuthService auth,
        McpClient mcp,
        ILogger<KbController> logger)
    {
        _db = db;
        _storage = storage;
        _qdrant = qdrant;
        _auth = auth;
        _mcp = mcp;
        _logger = logger;
    }

    private async Task<bool> RequireAdminAsync(CancellationToken ct)
    {
        var token = Request.Headers["X-Kb-Token"].ToString();
        return await _auth.ValidateAsync(token, ct);
    }

    [HttpGet("collections")]
    public async Task<IActionResult> GetCollections(CancellationToken ct)
    {
        var items = await _db.KbCollections
            .Where(c => c.IsActive)
            .OrderBy(c => c.CollectionKey)
            .Select(c => new
            {
                key = c.CollectionKey,
                name = c.DisplayName,
                qdrant = c.QdrantName,
                description = c.Description
            })
            .ToListAsync(ct);

        var docCounts = await _db.KbDocuments
            .Where(d => d.Status != "disabled")
            .GroupBy(d => d.CollectionKey)
            .Select(g => new { Key = g.Key, Count = g.Count(), Chunks = g.Sum(x => x.ChunkCount) })
            .ToListAsync(ct);

        var byKey = docCounts.ToDictionary(x => x.Key, x => (x.Count, x.Chunks));
        var result = items.Select(c =>
        {
            byKey.TryGetValue(c.key, out var counts);
            return new { c.key, c.name, c.qdrant, c.description, docCount = counts.Count, chunkCount = counts.Chunks };
        });
        return Ok(new { success = true, items = result });
    }

    [HttpPost("upload")]
    [RequestSizeLimit(500_000_000)]
    public async Task<IActionResult> Upload(
        [FromForm] IFormFile file,
        [FromForm] string collectionKey,
        [FromForm] string? title,
        [FromForm] string? tags,
        CancellationToken ct)
    {
        if (!await RequireAdminAsync(ct))
            return Unauthorized(new { success = false, error = "unauthorized" });

        if (file == null || file.Length == 0)
            return BadRequest(new { success = false, error = "file required" });
        if (string.IsNullOrWhiteSpace(collectionKey))
            return BadRequest(new { success = false, error = "collectionKey required" });

        var coll = await _db.KbCollections.FirstOrDefaultAsync(c => c.CollectionKey == collectionKey, ct);
        if (coll == null) return BadRequest(new { success = false, error = "unknown collectionKey" });

        await using var stream = file.OpenReadStream();
        var stored = await _storage.SaveAsync(stream, file.FileName, ct);

        var tagArr = (tags ?? "")
            .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Where(t => t.Length > 0).ToArray();

        var doc = new KbDocument
        {
            Id = Guid.NewGuid(),
            CollectionKey = collectionKey,
            Title = string.IsNullOrWhiteSpace(title) ? Path.GetFileNameWithoutExtension(file.FileName) : title.Trim(),
            OriginalPath = stored.RelativePath,
            FileSha256 = stored.Sha256,
            FileSize = stored.Size,
            MimeType = file.ContentType,
            Tags = tagArr,
            Status = "pending",
            ChunkCount = 0,
            UploadedAt = DateTime.UtcNow,
            UploadedBy = HttpContext.Connection.RemoteIpAddress?.ToString()
        };
        _db.KbDocuments.Add(doc);

        _db.KbIngestJobs.Add(new KbIngestJob
        {
            DocId = doc.Id,
            Stage = "parse",
            EnqueuedAt = DateTime.UtcNow
        });
        await _db.SaveChangesAsync(ct);

        _logger.LogInformation("[Kb] 업로드 {Id} {Title} ({Size} bytes)", doc.Id, doc.Title, doc.FileSize);
        return Ok(new { success = true, docId = doc.Id, status = doc.Status });
    }

    [HttpGet("documents")]
    public async Task<IActionResult> GetDocuments(
        [FromQuery] string? collection,
        [FromQuery] string? status,
        [FromQuery] string? q,
        [FromQuery] int page = 0,
        [FromQuery] int pageSize = 50,
        CancellationToken ct = default)
    {
        page = Math.Max(0, page);
        pageSize = Math.Clamp(pageSize, 1, 200);

        var query = _db.KbDocuments.AsNoTracking().AsQueryable();
        if (!string.IsNullOrWhiteSpace(collection)) query = query.Where(d => d.CollectionKey == collection);
        if (!string.IsNullOrWhiteSpace(status))     query = query.Where(d => d.Status == status);
        if (!string.IsNullOrWhiteSpace(q))
        {
            var like = $"%{q}%";
            query = query.Where(d => EF.Functions.ILike(d.Title, like));
        }

        var total = await query.CountAsync(ct);
        var items = await query
            .OrderByDescending(d => d.UploadedAt)
            .Skip(page * pageSize)
            .Take(pageSize)
            .Select(d => new
            {
                id = d.Id,
                title = d.Title,
                collection = d.CollectionKey,
                tags = d.Tags,
                status = d.Status,
                chunkCount = d.ChunkCount,
                fileSize = d.FileSize,
                uploadedAt = d.UploadedAt,
                indexedAt = d.IndexedAt,
                errorMessage = d.ErrorMessage
            })
            .ToListAsync(ct);
        return Ok(new { success = true, total, page, pageSize, items });
    }

    [HttpGet("documents/{id:guid}")]
    public async Task<IActionResult> GetDocument(Guid id, CancellationToken ct)
    {
        var d = await _db.KbDocuments.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
        if (d == null) return NotFound(new { success = false });
        return Ok(new
        {
            success = true,
            item = new
            {
                id = d.Id,
                title = d.Title,
                collection = d.CollectionKey,
                tags = d.Tags,
                status = d.Status,
                chunkCount = d.ChunkCount,
                fileSize = d.FileSize,
                mimeType = d.MimeType,
                uploadedAt = d.UploadedAt,
                indexedAt = d.IndexedAt,
                disabledAt = d.DisabledAt,
                originalPath = d.OriginalPath,
                fileSha256 = d.FileSha256,
                errorMessage = d.ErrorMessage
            }
        });
    }

    [HttpGet("documents/{id:guid}/chunks")]
    public async Task<IActionResult> GetChunks(Guid id, [FromQuery] int limit = 200, CancellationToken ct = default)
    {
        if (!await RequireAdminAsync(ct))
            return Unauthorized(new { success = false, error = "unauthorized" });

        var d = await _db.KbDocuments.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
        if (d == null) return NotFound(new { success = false });

        var coll = await _db.KbCollections.AsNoTracking()
            .FirstOrDefaultAsync(c => c.CollectionKey == d.CollectionKey, ct);
        if (coll == null) return BadRequest(new { success = false, error = "collection not found" });

        limit = Math.Clamp(limit, 1, 500);
        var rows = await _qdrant.GetChunksByDocIdAsync(coll.QdrantName, d.Id, limit, ct);

        static string? GetString(Dictionary<string, System.Text.Json.JsonElement> p, string key)
        {
            if (!p.TryGetValue(key, out var v)) return null;
            return v.ValueKind == System.Text.Json.JsonValueKind.String ? v.GetString() : v.ToString();
        }

        var chunks = rows.Select(p => new
        {
            text = GetString(p, "text") ?? "",
            chunk_kind = GetString(p, "chunk_kind") ?? GetString(p, "kind"),
            locator = GetString(p, "locator"),
            page = GetString(p, "page"),
            sheet = GetString(p, "sheet"),
            heading_path = GetString(p, "heading_path")
        }).ToList();

        return Ok(new
        {
            success = true,
            docId = d.Id,
            collection = d.CollectionKey,
            count = chunks.Count,
            chunks
        });
    }

    [HttpGet("jobs")]
    public async Task<IActionResult> GetJobs(
        [FromQuery] Guid? docId,
        [FromQuery] string? stage,
        [FromQuery] bool pendingOnly = false,
        CancellationToken ct = default)
    {
        var q = _db.KbIngestJobs.AsNoTracking().AsQueryable();
        if (docId.HasValue) q = q.Where(j => j.DocId == docId.Value);
        if (!string.IsNullOrWhiteSpace(stage)) q = q.Where(j => j.Stage == stage);
        if (pendingOnly) q = q.Where(j => j.FinishedAt == null);

        var items = await q.OrderByDescending(j => j.EnqueuedAt).Take(200).Select(j => new
        {
            id = j.Id,
            docId = j.DocId,
            stage = j.Stage,
            attempts = j.Attempts,
            lastError = j.LastError,
            enqueuedAt = j.EnqueuedAt,
            startedAt = j.StartedAt,
            finishedAt = j.FinishedAt
        }).ToListAsync(ct);
        return Ok(new { success = true, items });
    }

    [HttpGet("download/{id:guid}")]
    public async Task<IActionResult> Download(Guid id, CancellationToken ct)
    {
        var d = await _db.KbDocuments.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
        if (d == null) return NotFound();
        var abs = _storage.Resolve(d.OriginalPath);
        if (!System.IO.File.Exists(abs)) return NotFound();

        var stream = new FileStream(abs, FileMode.Open, FileAccess.Read, FileShare.Read, 64 * 1024, true);
        var ext = Path.GetExtension(abs);
        var fileName = string.IsNullOrEmpty(ext) ? d.Title : d.Title + ext;
        return File(stream, d.MimeType ?? "application/octet-stream", fileName);
    }

    [HttpGet("instruments/download")]
    public IActionResult DownloadInstrumentsDraft([FromQuery] string path)
    {
        var abs = _storage.Resolve(path);
        if (!System.IO.File.Exists(abs)) return NotFound(new { success = false, error = "file not found" });

        var stream = new FileStream(abs, FileMode.Open, FileAccess.Read, FileShare.Read, 64 * 1024, true);
        var fileName = Path.GetFileName(abs);
        return File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName);
    }

    [HttpDelete("documents/{id:guid}")]
    public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
    {
        if (!await RequireAdminAsync(ct))
            return Unauthorized(new { success = false, error = "unauthorized" });

        var d = await _db.KbDocuments.FirstOrDefaultAsync(x => x.Id == id, ct);
        if (d == null) return NotFound(new { success = false });

        var coll = await _db.KbCollections.FirstOrDefaultAsync(c => c.CollectionKey == d.CollectionKey, ct);
        if (coll != null)
            await _qdrant.DeleteByDocAsync(coll.QdrantName, d.Id, ct);

        _storage.Delete(d.OriginalPath);

        _db.KbDocuments.Remove(d);
        await _db.SaveChangesAsync(ct);
        return Ok(new { success = true });
    }

    [HttpPost("documents/{id:guid}/reindex")]
    public async Task<IActionResult> Reindex(Guid id, CancellationToken ct)
    {
        if (!await RequireAdminAsync(ct))
            return Unauthorized(new { success = false, error = "unauthorized" });

        var d = await _db.KbDocuments.FirstOrDefaultAsync(x => x.Id == id, ct);
        if (d == null) return NotFound(new { success = false });

        var coll = await _db.KbCollections.FirstOrDefaultAsync(c => c.CollectionKey == d.CollectionKey, ct);
        if (coll != null)
            await _qdrant.DeleteByDocAsync(coll.QdrantName, d.Id, ct);

        d.Status = "pending";
        d.ChunkCount = 0;
        d.ErrorMessage = null;
        d.IndexedAt = null;

        _db.KbIngestJobs.Add(new KbIngestJob { DocId = d.Id, Stage = "parse" });
        await _db.SaveChangesAsync(ct);
        return Ok(new { success = true });
    }

    [HttpPost("documents/{id:guid}/disable")]
    public async Task<IActionResult> Disable(Guid id, CancellationToken ct)
    {
        if (!await RequireAdminAsync(ct))
            return Unauthorized(new { success = false, error = "unauthorized" });

        var d = await _db.KbDocuments.FirstOrDefaultAsync(x => x.Id == id, ct);
        if (d == null) return NotFound(new { success = false });
        d.Status = "disabled";
        d.DisabledAt = DateTime.UtcNow;
        await _db.SaveChangesAsync(ct);
        return Ok(new { success = true });
    }

    public sealed record BulkDisableRequest(string Title);

    [HttpPost("documents/bulk-disable")]
    public async Task<IActionResult> BulkDisable([FromBody] BulkDisableRequest req, CancellationToken ct)
    {
        if (!await RequireAdminAsync(ct))
            return Unauthorized(new { success = false, error = "unauthorized" });
        if (req == null || string.IsNullOrWhiteSpace(req.Title))
            return BadRequest(new { success = false, error = "title required" });

        var rows = await _db.KbDocuments
            .Where(d => d.Title == req.Title && d.Status != "disabled")
            .ExecuteUpdateAsync(set => set
                .SetProperty(x => x.Status, _ => "disabled")
                .SetProperty(x => x.DisabledAt, _ => DateTime.UtcNow), ct);
        return Ok(new { success = true, affected = rows });
    }

    public sealed record PurgeRequest(int? OlderThanDays);

    [HttpPost("documents/purge-disabled")]
    public async Task<IActionResult> PurgeDisabled([FromBody] PurgeRequest req, CancellationToken ct)
    {
        if (!await RequireAdminAsync(ct))
            return Unauthorized(new { success = false, error = "unauthorized" });

        var cutoff = req?.OlderThanDays is int days && days > 0
            ? DateTime.UtcNow.AddDays(-days)
            : (DateTime?)null;

        var query = _db.KbDocuments.Where(d => d.Status == "disabled");
        if (cutoff.HasValue) query = query.Where(d => d.DisabledAt != null && d.DisabledAt < cutoff);

        var victims = await query.ToListAsync(ct);
        foreach (var d in victims)
        {
            var coll = await _db.KbCollections.FirstOrDefaultAsync(c => c.CollectionKey == d.CollectionKey, ct);
            if (coll != null) await _qdrant.DeleteByDocAsync(coll.QdrantName, d.Id, ct);
            _storage.Delete(d.OriginalPath);
        }
        _db.KbDocuments.RemoveRange(victims);
        await _db.SaveChangesAsync(ct);
        return Ok(new { success = true, deleted = victims.Count });
    }

    public sealed record InferInstrumentsRequest(bool UseLlm = false, string? SeedDocId = null);

    [HttpPost("instruments/infer")]
    public async Task<IActionResult> InferInstruments([FromBody] InferInstrumentsRequest? req, CancellationToken ct)
    {
        if (!await RequireAdminAsync(ct))
            return Unauthorized(new { success = false, error = "unauthorized" });

        try
        {
            var args = new Dictionary<string, object>
            {
                ["use_llm"] = req?.UseLlm ?? false,
                ["seed_doc_id"] = req?.SeedDocId ?? ""
            };

            // [M-3 수정] seed_doc_id 머지 — 기존 Excel 경로 전달
            string? seedId = req?.SeedDocId;
            if (!string.IsNullOrWhiteSpace(seedId))
            {
                var seedDoc = await _db.KbDocuments
                    .FirstOrDefaultAsync(d => d.Id == Guid.Parse(seedId), ct);
                if (seedDoc != null)
                {
                    var seedPath = _storage.Resolve(seedDoc.OriginalPath);
                    if (System.IO.File.Exists(seedPath))
                    {
                        args["seed_excel_path"] = seedPath;
                    }
                }
            }

            var raw = await _mcp.CallToolAsync("infer_field_instruments", args, ct);
            if (string.IsNullOrWhiteSpace(raw))
                return BadRequest(new { success = false, error = "MCP 응답 없음" });

            // [H-2 수정] McpClient.CallToolAsync는 실패 시 평문 문자열 반환
            try
            {
                using var jdoc = System.Text.Json.JsonDocument.Parse(raw);
                if (jdoc.RootElement.TryGetProperty("success", out var s) && s.ValueKind == System.Text.Json.JsonValueKind.False)
                {
                    var err = jdoc.RootElement.TryGetProperty("message", out var e) ? e.GetString() : "unknown";
                    return BadRequest(new { success = false, error = err });
                }

                var docPath = jdoc.RootElement.GetProperty("doc_path").GetString() ?? "";
                var instrumentCount = jdoc.RootElement.GetProperty("instrument_count").GetInt32();
                var unmatchedCount = jdoc.RootElement.GetProperty("unmatched_count").GetInt32();
                var powerEquipmentCount = jdoc.RootElement.TryGetProperty("power_equipment_count", out var pec) ? pec.GetInt32() : 0;

                // Excel 파일을 KB 저장소로 복사 (하지만 RAG 인덱싱은 안 함)
                var fileName = System.IO.Path.GetFileName(docPath);
                await using var stream = new FileStream(docPath, FileMode.Open, FileAccess.Read, FileShare.Read);
                var stored = await _storage.SaveAsync(stream, fileName, ct);

                // [L-2 수정] /tmp 임시 파일 정리
                try { System.IO.File.Delete(docPath); } catch { }

                // KbDocument/KbIngestJob INSERT 안 함 — 초안은 사용자가 수동으로 KB에 업로드해야 함

                _logger.LogInformation("[Kb][Infer] {Count} instruments, {Power} power equipment, {Unmatched} unmatched", instrumentCount, powerEquipmentCount, unmatchedCount);

                return Ok(new
                {
                    success = true,
                    downloadPath = stored.RelativePath,
                    instrumentCount,
                    powerEquipmentCount,
                    unmatchedCount,
                    message = jdoc.RootElement.TryGetProperty("message", out var m) ? m.GetString() : ""
                });
            }
            catch (System.Text.Json.JsonException)
            {
                // [H-2 수정] 평문 응답("도구 호출 실패: ...")일 경우
                _logger.LogWarning("[Kb][Infer] MCP 평문 응답: {Raw}", raw);
                return BadRequest(new { success = false, error = "MCP 도구 실패: " + raw.TrimEnd('\r', '\n', ' ') });
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[Kb][Infer] 실패");
            return BadRequest(new { success = false, error = ex.Message });
        }
    }
}
