using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Prometheus;
using StackExchange.Redis;

public class TrendDetector
{
    private readonly IConnectionMultiplexer _redis;
    private readonly IServiceProvider _services;
    private readonly TrendDetectionOptions _options;
    private readonly ClinicalMetrics _metrics;
    private readonly ILogger<TrendDetector> _logger;

    public TrendDetector(
        ILogger<TrendDetector> logger, 
        ClinicalMetrics metrics,
        IServiceProvider services, 
        IConnectionMultiplexer redis,
        IOptions<TrendDetectionOptions> options)
    {
        _logger = logger;
        _metrics = metrics;
        _services = services;
        _redis = redis;
        _options = options.Value;
    }

    public async Task<TrendResult> ProcessObservationAsync(
        Guid encounterId,
        Guid patientId,
        string observationCode,
        decimal value,
        DateTimeOffset recordedAt,
        CancellationToken ct = default)
    {
        if (!TrendCalculator.IsTrendCode(observationCode))
            return new TrendResult(TrendOutcome.NotTrendCode);

        using var timer = _metrics.TrendAnalysisDuration.NewTimer();

        var cache = _redis.GetDatabase();
        var key = TrendCalculator.HistoryKey(encounterId, observationCode);

        // Append new entry to history list
        var entry = new TrendHistoryEntry(value, recordedAt);
        var historyJson = await cache.StringGetAsync(key);
        var history = historyJson.HasValue
            ? JsonSerializer.Deserialize<List<TrendHistoryEntry>>(historyJson!) ?? new()
            : new List<TrendHistoryEntry>();

        history.Add(entry);

        // Trim to max entries and evict entries outside window
        var cutoff = recordedAt.AddMinutes(-_options.WindowMinutes);
        history = history
            .Where(e => e.RecordedAt >= cutoff)
            .TakeLast(_options.MaxHistoryEntries)
            .ToList();

        await cache.StringSetAsync(
            key,
            JsonSerializer.Serialize(history),
            TimeSpan.FromSeconds(_options.HistoryTtlSeconds));

        if (history.Count < 2)
            return new TrendResult(TrendOutcome.InsufficientHistory, observationCode);

        var rate = TrendCalculator.ComputeRatePerMinute(history, _options.WindowMinutes);
        if (rate is null)
            return new TrendResult(TrendOutcome.InsufficientHistory, observationCode);

        if (!_options.RateThresholdsPerMinute.TryGetValue(observationCode, out var threshold))
            return new TrendResult(TrendOutcome.Stable, observationCode, rate);

        if (!TrendCalculator.ExceedsThreshold(observationCode, rate.Value, threshold))
            return new TrendResult(TrendOutcome.Stable, observationCode, rate);

        using (var scope = _services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            if (!await db.Encounters.AnyAsync(e => e.Id == encounterId, ct))
            {
                _logger.LogWarning(
                    "Skipping trend alert for unknown encounter {EncounterId}", encounterId);
                return new TrendResult(TrendOutcome.EncounterNotFound, observationCode, rate);
            }
        }

        var trendContext = TrendContextBuilder.BuildContext(observationCode, history, value);
        var details = TrendCalculator.DescribeTrend(observationCode, rate.Value, value);

        var contributor = new List<ScoreContributor>
        {
            new()
            {
                Parameter = TrendCalculator.DisplayName(observationCode),
                Points = 0,
                RawValue = value.ToString(),
                NormalRange = null
            }
        };
        var created = await TryCreateAlertAsync(
            encounterId, patientId, observationCode, details, rate.Value,
            contributor, trendContext, ct);

        return new TrendResult(
            created ? TrendOutcome.RapidDeterioration : TrendOutcome.AlertAlreadyOpen,
            observationCode, rate, created, trendContext);
    }

    private async Task<bool> TryCreateAlertAsync(
        Guid encounterId, Guid patientId,
        string observationCode, string details, decimal ratePerMinute,
        List<ScoreContributor> contributors, TrendContext? trendContext,
        CancellationToken ct)
    {
        using var scope = _services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        await using var tx = await db.Database.BeginTransactionAsync(ct);

        var alertId = Guid.NewGuid();
        var triggeredAt = DateTimeOffset.UtcNow;
        var fullDetails = $"{details} — velocity {ratePerMinute:F2}/min over {_options.WindowMinutes}min window.";

        MedicationContext? medication = null;
        var correlation = scope.ServiceProvider.GetRequiredService<MedicationCorrelationHelper>();
        medication = await correlation.TryGetContextAsync(encounterId, observationCode, ct);

        var explanation = AlertExplanationBuilder.Build(
            "Rapid deterioration", null, contributors, trendContext, medication);

        var inserted = await ClinicalAlertFactory.TryInsertAsync(
            db, alertId, encounterId, patientId,
            AlertType.RapidDeterioration, AlertSeverity.Warning,
            fullDetails, explanation, observationCode, triggeredAt, ct);

        if (!inserted)
        {
            await tx.RollbackAsync(ct);
            return false;
        }

        db.OutboxEvents.Add(new OutboxEvent
        {
            Id = Guid.NewGuid(),
            Topic = "alert.generated",
            Payload = ClinicalAlertFactory.SerializeOutboxPayload(new
            {
                alertId,
                encounterId,
                patientId,
                alertType = "RAPID_DETERIORATION",
                severity = "Warning",
                details = fullDetails,
                triggeredAt,
                observationCode,
                ratePerMinute,
                explanation,
                partitionKey = encounterId.ToString()
            }),
            PartitionKey = encounterId.ToString(),
            CreatedAt = DateTimeOffset.UtcNow
        });

        await db.SaveChangesAsync(ct);
        await tx.CommitAsync(ct);

        _metrics.TrendAlertsTotal.WithLabels(observationCode).Inc();
        _metrics.ClinicalAlertsTotal
            .WithLabels("RAPID_DETERIORATION", "Warning").Inc();

        return true;
    }
}