using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

/// <summary>
/// Polls for uploaded batches without OCR results and pre-fills draft fields.
/// OCR is opt-in and non-blocking: failures leave the batch in UPLOADED status
/// for manual entry.
/// </summary>
public class OcrProcessingService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly OcrOptions _options;
    private readonly ILogger<OcrProcessingService> _logger;

    public OcrProcessingService(
        IServiceScopeFactory scopeFactory,
        IOptions<OcrOptions> options,
        ILogger<OcrProcessingService> logger)
    {
        _scopeFactory = scopeFactory;
        _options = options.Value;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "OcrProcessingService started. Provider: {Provider}, poll interval: {PollInterval}s, confidence threshold: {Threshold}",
            _options.Provider,
            _options.PollIntervalSeconds,
            _options.ConfidenceThreshold);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await ProcessPendingBatchesAsync(stoppingToken);
            }
            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
            {
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "OCR processing cycle failed");
            }

            await Task.Delay(
                TimeSpan.FromSeconds(_options.PollIntervalSeconds), stoppingToken);
        }

        _logger.LogInformation("OcrProcessingService stopped");
    }

    private async Task ProcessPendingBatchesAsync(CancellationToken ct)
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var ocr = scope.ServiceProvider.GetRequiredService<IOcrService>();
        var storage = scope.ServiceProvider.GetRequiredService<IDocumentStorageService>();
        var preFiller = scope.ServiceProvider.GetRequiredService<OcrDraftPreFiller>();

        var pendingBatches = await db.DigitizationBatches
            .AsNoTracking()
            .Where(b => b.Status == BatchStatus.Uploaded)
            .Where(b => !db.OcrResults.Any(o => o.BatchId == b.Id))
            .Where(b => !db.DigitizationEvents.Any(e =>
                e.BatchId == b.Id &&
                (e.EventType == DigitizationEventType.OcrCompleted ||
                 e.EventType == DigitizationEventType.OcrFailed)))
            .OrderBy(b => b.CreatedAt)
            .Take(5)
            .ToListAsync(ct);

        foreach (var batch in pendingBatches)
        {
            await ProcessBatchAsync(db, ocr, storage, preFiller, batch, ct);
        }
    }

    private async Task ProcessBatchAsync(
        AppDbContext db,
        IOcrService ocr,
        IDocumentStorageService storage,
        OcrDraftPreFiller preFiller,
        DigitizationBatch batch,
        CancellationToken ct)
    {
        var currentStatus = await db.DigitizationBatches
            .AsNoTracking()
            .Where(b => b.Id == batch.Id)
            .Select(b => b.Status)
            .FirstOrDefaultAsync(ct);

        if (currentStatus != BatchStatus.Uploaded)
        {
            _logger.LogDebug(
                "Skipping OCR for batch {BatchId}: status is {Status}",
                batch.Id, currentStatus.ToDbString());
            return;
        }

        var actorUserId = await db.DigitizationEvents
            .AsNoTracking()
            .Where(e => e.BatchId == batch.Id &&
                        (e.EventType == DigitizationEventType.Uploaded ||
                         e.EventType == DigitizationEventType.CorrectionUploaded))
            .OrderBy(e => e.OccurredAt)
            .Select(e => e.ActorUserId)
            .FirstOrDefaultAsync(ct);

        if (actorUserId == Guid.Empty)
        {
            _logger.LogWarning(
                "Skipping OCR for batch {BatchId}: no upload event found",
                batch.Id);
            return;
        }

        var document = await db.ScannedDocuments
            .AsNoTracking()
            .FirstOrDefaultAsync(d => d.BatchId == batch.Id, ct);

        if (document is null)
        {
            _logger.LogWarning(
                "Skipping OCR for batch {BatchId}: scanned document metadata not found",
                batch.Id);
            return;
        }

        var startedAt = DateTimeOffset.UtcNow;
        db.DigitizationEvents.Add(new DigitizationEvent
        {
            Id = Guid.NewGuid(),
            BatchId = batch.Id,
            EventType = DigitizationEventType.OcrStarted,
            ActorUserId = actorUserId,
            OccurredAt = startedAt,
            MetadataJson = JsonSerializer.Serialize(new
            {
                provider = _options.Provider
            })
        });
        await db.SaveChangesAsync(ct);

        try
        {
            await using var documentStream = await storage.DownloadAsync(document.ObjectKey);
            var extraction = await ocr.ExtractAsync(documentStream, document.ContentType);

            await preFiller.PreFillAsync(batch.Id, batch.BatchType, extraction);

            var fieldConfidences = extraction.Fields
                .GroupBy(f => f.FieldName, StringComparer.Ordinal)
                .ToDictionary(g => g.Key, g => g.First().Confidence, StringComparer.Ordinal);

            var processedAt = DateTimeOffset.UtcNow;
            db.OcrResults.Add(new OcrResult
            {
                Id = Guid.NewGuid(),
                BatchId = batch.Id,
                Provider = _options.Provider,
                FieldConfidencesJson = JsonSerializer.Serialize(fieldConfidences),
                RawText = extraction.RawText,
                DurationMs = extraction.DurationMs,
                ProcessedAt = processedAt
            });

            db.DigitizationEvents.Add(new DigitizationEvent
            {
                Id = Guid.NewGuid(),
                BatchId = batch.Id,
                EventType = DigitizationEventType.OcrCompleted,
                ActorUserId = actorUserId,
                OccurredAt = processedAt,
                MetadataJson = JsonSerializer.Serialize(new
                {
                    provider = _options.Provider,
                    durationMs = extraction.DurationMs,
                    fieldCount = extraction.Fields.Count,
                    confidentFieldCount = fieldConfidences.Count(kv => kv.Value >= _options.ConfidenceThreshold)
                })
            });

            await db.SaveChangesAsync(ct);

            _logger.LogInformation(
                "OCR completed for batch {BatchId}: provider={Provider}, fields={FieldCount}, duration={DurationMs}ms",
                batch.Id, _options.Provider, extraction.Fields.Count, extraction.DurationMs);
        }
        catch (Exception ex) when (ex is not OperationCanceledException)
        {
            db.DigitizationEvents.Add(new DigitizationEvent
            {
                Id = Guid.NewGuid(),
                BatchId = batch.Id,
                EventType = DigitizationEventType.OcrFailed,
                ActorUserId = actorUserId,
                OccurredAt = DateTimeOffset.UtcNow,
                MetadataJson = JsonSerializer.Serialize(new
                {
                    provider = _options.Provider,
                    error = ex.Message
                })
            });
            await db.SaveChangesAsync(ct);

            _logger.LogWarning(
                ex,
                "OCR failed for batch {BatchId}: {ErrorMessage}",
                batch.Id, ex.Message);
        }
    }
}
