using System.Text.Json;
using Hc900Crawler.Core.Domain.Entities;
using Hc900Crawler.Infrastructure.Database;
using Hc900Crawler.Infrastructure.Mcp;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Hc900Crawler.Infrastructure.Kb;

/// <summary>
/// kb_ingest_jobs 큐를 폴링하며 parse → embed → index 단계를 수행.
/// 한 잡(stage='parse')을 픽업해서 단일 트랜잭션 안에서 끝까지 진행한다.
/// </summary>
public sealed class KbIngestWorker : BackgroundService
{
    private readonly IServiceProvider _sp;
    private readonly KbQdrantClient _qdrant;
    private readonly KbEmbeddingClient _embed;
    private readonly McpClient _mcp;
    private readonly KbStorageService _storage;
    private readonly ILogger<KbIngestWorker> _logger;
    private readonly TimeSpan _pollInterval;
    private readonly int _maxAttempts;

    public KbIngestWorker(
        IServiceProvider sp,
        KbQdrantClient qdrant,
        KbEmbeddingClient embed,
        McpClient mcp,
        KbStorageService storage,
        IConfiguration config,
        ILogger<KbIngestWorker> logger)
    {
        _sp = sp;
        _qdrant = qdrant;
        _embed = embed;
        _mcp = mcp;
        _storage = storage;
        _logger = logger;

        var sec = int.TryParse(config["Kb:WorkerPollIntervalSeconds"], out var s) && s > 0 ? s : 2;
        _pollInterval = TimeSpan.FromSeconds(sec);
        _maxAttempts = int.TryParse(config["Kb:MaxAttempts"], out var m) && m > 0 ? m : 3;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("[Kb][Worker] 시작 (poll {Sec}s, maxAttempts {N})", _pollInterval.TotalSeconds, _maxAttempts);
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                var processed = await ProcessOneAsync(stoppingToken);
                if (!processed)
                    await Task.Delay(_pollInterval, stoppingToken);
            }
            catch (OperationCanceledException) { break; }
            catch (Exception ex)
            {
                _logger.LogError(ex, "[Kb][Worker] 루프 오류");
                try { await Task.Delay(_pollInterval, stoppingToken); } catch { break; }
            }
        }
    }

    private async Task<bool> ProcessOneAsync(CancellationToken ct)
    {
        using var scope = _sp.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<Hc900DbContext>();

        var job = await db.KbIngestJobs
            .Where(j => j.FinishedAt == null && j.Stage == "parse" && j.Attempts < _maxAttempts)
            .OrderBy(j => j.EnqueuedAt)
            .FirstOrDefaultAsync(ct);

        if (job == null) return false;

        job.StartedAt = DateTime.UtcNow;
        job.Attempts++;
        await db.SaveChangesAsync(ct);

        var doc = await db.KbDocuments.FirstOrDefaultAsync(d => d.Id == job.DocId, ct);
        if (doc == null)
        {
            job.FinishedAt = DateTime.UtcNow;
            job.LastError = "document not found";
            await db.SaveChangesAsync(ct);
            return true;
        }

        var coll = await db.KbCollections.FirstOrDefaultAsync(c => c.CollectionKey == doc.CollectionKey, ct);
        if (coll == null)
        {
            await FailAsync(db, job, doc, "collection not found", ct);
            return true;
        }

        try
        {
            // 1) parse
            doc.Status = "parsing";
            doc.ErrorMessage = null;
            await db.SaveChangesAsync(ct);

            var abs = _storage.Resolve(doc.OriginalPath);
            var chunks = await ParseAsync(doc, coll, abs, ct);
            if (chunks == null || chunks.Count == 0)
                throw new Exception("파싱 결과 청크 0건");

            _logger.LogInformation("[Kb][Worker] {Id} parse: {N} chunks", doc.Id, chunks.Count);

            // 2) embed — 단일 청크 실패는 skip, 모두 실패해야 진짜 실패
            doc.Status = "embedding";
            await db.SaveChangesAsync(ct);

            var points = new List<QdrantPoint>(chunks.Count);
            int skipped = 0;
            foreach (var c in chunks)
            {
                var vec = await _embed.EmbedAsync(c.Text, ct);
                if (vec == null) { skipped++; continue; }
                points.Add(new QdrantPoint
                {
                    id = Guid.NewGuid(),
                    vector = vec,
                    payload = new Dictionary<string, object?>
                    {
                        ["doc_id"] = doc.Id.ToString(),
                        ["collection_key"] = doc.CollectionKey,
                        ["title"] = doc.Title,
                        ["text"] = c.Text,
                        ["chunk_kind"] = c.ChunkKind,
                        ["locator"] = c.Locator,
                        ["uploaded_at"] = doc.UploadedAt.ToString("O"),
                        ["tags"] = doc.Tags
                    }
                });
            }
            if (points.Count == 0)
                throw new Exception($"임베딩 실패: 청크 {chunks.Count}건 전부 실패");
            if (skipped > 0)
                _logger.LogWarning("[Kb][Worker] {Id} 임베딩 일부 실패: {Skipped}/{Total} 건 누락", doc.Id, skipped, chunks.Count);

            // 3) index
            doc.Status = "indexed";
            var ok = await _qdrant.UpsertAsync(coll.QdrantName, points, ct);
            if (!ok) throw new Exception("Qdrant upsert 실패");

            doc.ChunkCount = points.Count;
            doc.IndexedAt = DateTime.UtcNow;
            doc.ErrorMessage = skipped > 0
                ? $"부분 인덱싱: {points.Count}/{chunks.Count} 청크 (임베딩 {skipped}건 실패)"
                : null;

            job.FinishedAt = DateTime.UtcNow;
            job.LastError = null;
            await db.SaveChangesAsync(ct);
            _logger.LogInformation("[Kb][Worker] {Id} indexed ({N} chunks)", doc.Id, chunks.Count);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "[Kb][Worker] {Id} 처리 실패 (attempt {A}/{M})", doc.Id, job.Attempts, _maxAttempts);
            await FailAsync(db, job, doc, ex.Message, ct);
        }

        return true;
    }

    private async Task FailAsync(Hc900DbContext db, KbIngestJob job, KbDocument doc, string error, CancellationToken ct)
    {
        job.LastError = error;
        if (job.Attempts >= _maxAttempts)
        {
            job.FinishedAt = DateTime.UtcNow;
            doc.Status = "failed";
            doc.ErrorMessage = error;
        }
        else
        {
            // 재시도 대기 — finished_at NULL, attempts 누적
        }
        await db.SaveChangesAsync(ct);
    }

    private sealed record ParsedChunk(string Text, string ChunkKind, string Locator);

    private async Task<List<ParsedChunk>?> ParseAsync(KbDocument doc, KbCollection coll, string absPath, CancellationToken ct)
    {
        var args = new Dictionary<string, object>
        {
            ["doc_id"] = doc.Id.ToString(),
            ["title"] = doc.Title,
            ["file_path"] = absPath,
            ["mime_type"] = doc.MimeType ?? "",
            ["collection_key"] = doc.CollectionKey,
            ["chunking_policy"] = coll.ChunkingPolicy
        };

        var raw = await _mcp.CallToolAsync("parse_document", args, ct);
        if (string.IsNullOrWhiteSpace(raw))
            throw new Exception("MCP parse_document 응답 없음");

        // FastMCP는 텍스트 컨텐츠를 단순 JSON 문자열로 돌려준다.
        try
        {
            using var jdoc = JsonDocument.Parse(raw);
            if (jdoc.RootElement.TryGetProperty("success", out var s) && s.ValueKind == JsonValueKind.False)
            {
                var err = jdoc.RootElement.TryGetProperty("error", out var e) ? e.GetString() : "unknown";
                throw new Exception($"MCP parse_document 실패: {err}");
            }

            if (!jdoc.RootElement.TryGetProperty("chunks", out var arr) || arr.ValueKind != JsonValueKind.Array)
                return new List<ParsedChunk>();

            var list = new List<ParsedChunk>();
            foreach (var c in arr.EnumerateArray())
            {
                var text = c.TryGetProperty("text", out var t) ? (t.GetString() ?? "") : "";
                if (string.IsNullOrWhiteSpace(text)) continue;
                var kind = c.TryGetProperty("chunk_kind", out var k) ? (k.GetString() ?? "section") : "section";
                var loc  = c.TryGetProperty("locator", out var l)  ? (l.GetString() ?? "") : "";
                list.Add(new ParsedChunk(text, kind, loc));
            }
            return list;
        }
        catch (JsonException)
        {
            throw new Exception("MCP parse_document JSON 파싱 실패");
        }
    }
}
