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

public class News2Detector
{
    private const int News2TtlSeconds = 14400;  // 4 hours

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

    private readonly IConnectionMultiplexer _redis;
    private readonly IServiceProvider _services;
    private readonly ClinicalMetrics _metrics;
    private readonly ILogger<News2Detector> _logger;

    public News2Detector(
        IConnectionMultiplexer redis,
        IServiceProvider services,
        ClinicalMetrics metrics,
        ILogger<News2Detector> logger)
    {
        _redis = redis;
        _services = services;
        _metrics = metrics;
        _logger = logger;
    }

    public async Task<News2Result> ProcessObservationAsync(
        Guid encounterId,
        Guid patientId,
        string observationCode,
        decimal value,
        CancellationToken ct = default)
    {
        if (!News2Calculator.IsNews2Code(observationCode))
            return News2Result.NotNews2Code;

        using var timer = _metrics.News2ScoringDuration.NewTimer();
        var cache = _redis.GetDatabase();

        // GCS components are cached by GcsDetector — trigger re-score only
        if (!GcsCalculator.IsGcsCode(observationCode))
        {
            var individualScore = News2Calculator.ScoreParameter(observationCode, value);
            var paramData = JsonSerializer.Serialize(new
            {
                value,
                score = individualScore,
                recordedAt = DateTimeOffset.UtcNow
            }, CachedParamJsonOptions);
            await cache.StringSetAsync(
                News2Calculator.ParameterKey(encounterId, observationCode),
                paramData,
                TimeSpan.FromSeconds(News2TtlSeconds));
        }

        return await TryComputeScoreAsync(encounterId, patientId, ct);
    }

    private async Task<News2Result> TryComputeScoreAsync(
        Guid encounterId, Guid patientId, CancellationToken ct)
    {
        var cache = _redis.GetDatabase();
        var allKeys = News2Calculator.AllParameterKeys(encounterId);
        var allValues = await cache.StringGetAsync(allKeys);

        var scores = new int?[7];
        var rawValues = new string?[7];

        for (int i = 0; i < 7; i++)
        {
            if (i == 4)  // consciousness — GCS-first, AVPU-fallback
            {
                scores[4] = await ResolveConsciousnessScoreAsync(encounterId);
                rawValues[4] = await ResolveConsciousnessRawValueAsync(encounterId);
                if (scores[4] is null)
                {
                    var present = allValues.Count(v => v.HasValue) + 0;
                    _logger.LogDebug(
                        "NEWS2 incomplete for encounter {Id}: consciousness missing ({Present}/7 present)",
                        encounterId, present);
                    return News2Result.IncompleteParameters(present);
                }
                continue;
            }

            if (!allValues[i].HasValue)
            {
                _logger.LogDebug(
                    "NEWS2 incomplete for encounter {Id}: {Code} missing ({Present}/7 present)",
                    encounterId, News2Calculator.ParameterCodes[i],
                    allValues.Count(v => v.HasValue));
                return News2Result.IncompleteParameters(allValues.Count(v => v.HasValue));
            }

            var cached = JsonSerializer.Deserialize<News2CachedParam>(allValues[i]!, CachedParamJsonOptions);
            scores[i] = cached?.Score;
            rawValues[i] = cached?.Value.ToString();
        }

        var paramScores = scores.Select(s => s!.Value).ToArray();
        var contributors = News2ContributorBuilder.Build(paramScores, rawValues);
        var totalScore = paramScores.Sum();
        var hasSingleParamThree = paramScores.Any(s => s == 3);
        var riskLevel = News2Calculator.DetermineRiskLevel(totalScore, hasSingleParamThree);

        using (var scope = _services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            if (!await db.Encounters.AnyAsync(e => e.Id == encounterId, ct))
            {
                _logger.LogWarning(
                    "Skipping NEWS2 score for unknown encounter {EncounterId}", encounterId);
                return News2Result.EncounterNotFound;
            }
        }

        await PersistScoreAsync(
            encounterId, patientId, totalScore, riskLevel,
            paramScores, hasSingleParamThree, ct);

        var alertCreated = false;
        if (riskLevel == "HIGH")
        {
            alertCreated = await TryCreateAlertAsync(
                encounterId, patientId, AlertType.News2Emergency, AlertSeverity.Critical,
                totalScore, riskLevel, paramScores, contributors, ct);
        }
        else if (riskLevel == "MEDIUM" || riskLevel == "LOW_MEDIUM")
        {
            alertCreated = await TryCreateAlertAsync(
                encounterId, patientId, AlertType.News2Warning, AlertSeverity.Warning,
                totalScore, riskLevel, paramScores, contributors, ct);
        }

        return new News2Result(
            News2Outcome.ScoreComputed, totalScore, riskLevel, alertCreated, 7, hasSingleParamThree, contributors);
    }

    private async Task<int?> ResolveConsciousnessScoreAsync(Guid encounterId)
    {
        var cache = _redis.GetDatabase();
        var gcsValues = await cache.StringGetAsync(GcsCalculator.AllComponentKeys(encounterId));

        if (gcsValues.All(v => v.HasValue))
        {
            var eye = decimal.Parse(gcsValues[0]!);
            var verbal = decimal.Parse(gcsValues[1]!);
            var motor = decimal.Parse(gcsValues[2]!);
            var total = GcsCalculator.ComputeTotal(eye, verbal, motor)!.Value;
            return News2Calculator.ScoreConsciousnessFromGcs(total);
        }

        var avpuVal = await cache.StringGetAsync(
            News2Calculator.ParameterKey(encounterId, "AVPU"));
        if (!avpuVal.HasValue)
            return null;

        var cached = JsonSerializer.Deserialize<News2CachedParam>(avpuVal!, CachedParamJsonOptions);
        return cached?.Score;
    }

    private async Task<string?> ResolveConsciousnessRawValueAsync(Guid encounterId)
    {
        var cache = _redis.GetDatabase();
        var gcsValues = await cache.StringGetAsync(GcsCalculator.AllComponentKeys(encounterId));

        if (gcsValues.All(v => v.HasValue))
        {
            var eye = decimal.Parse(gcsValues[0]!);
            var verbal = decimal.Parse(gcsValues[1]!);
            var motor = decimal.Parse(gcsValues[2]!);
            return GcsCalculator.ComputeTotal(eye, verbal, motor)!.Value.ToString();
        }

        var avpuVal = await cache.StringGetAsync(
            News2Calculator.ParameterKey(encounterId, "AVPU"));
        if (!avpuVal.HasValue)
            return null;

        var cached = JsonSerializer.Deserialize<News2CachedParam>(avpuVal!, CachedParamJsonOptions);
        return cached?.Value.ToString();
    }

    private async Task<Guid> PersistScoreAsync(
        Guid encounterId, Guid patientId,
        int totalScore, string riskLevel,
        int[] paramScores, bool hasSingleParamThree,
        CancellationToken ct)
    {
        using var scope = _services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        var score = new News2Score
        {
            Id = Guid.NewGuid(),
            EncounterId = encounterId,
            PatientId = patientId,
            TotalScore = totalScore,
            RiskLevel = riskLevel,
            RespRateScore = paramScores[0],
            Spo2Score = paramScores[1],
            SystolicBpScore = paramScores[2],
            HeartRateScore = paramScores[3],
            ConsciousnessScore = paramScores[4],
            TemperatureScore = paramScores[5],
            SupplementalO2Score = paramScores[6],
            HasSingleParamThree = hasSingleParamThree,
            CalculatedAt = DateTimeOffset.UtcNow
        };

        db.News2Scores.Add(score);
        await db.SaveChangesAsync(ct);

        _metrics.News2ScoresTotal.WithLabels(riskLevel).Inc();

        return score.Id;
    }

    private async Task<bool> TryCreateAlertAsync(
        Guid encounterId, Guid patientId,
        AlertType alertType, AlertSeverity severity,
        int totalScore, string riskLevel, int[] paramScores,
        List<ScoreContributor> contributors,
        CancellationToken ct)
    {
        if (alertType == AlertType.News2Warning)
        {
            var suppression = _services.GetRequiredService<IAlertSuppressionService>();
            if (await suppression.IsSuppressedAsync(encounterId, alertType, ct))
            {
                _logger.LogDebug("NEWS2_WARNING suppressed for encounter {Id}", encounterId);
                return false;
            }
        }

        AlertCreationGuard.EnsureAllowed(alertType);

        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 details = BuildDetails(totalScore, riskLevel, paramScores);

        var correlation = scope.ServiceProvider.GetRequiredService<MedicationCorrelationHelper>();
        MedicationContext? medication = null;
        foreach (var (code, score) in News2Calculator.ParameterCodes.Zip(paramScores))
        {
            if (score <= 0)
                continue;

            medication = await correlation.TryGetContextAsync(encounterId, code, ct);
            if (medication is not null)
                break;
        }

        var explanation = AlertExplanationBuilder.Build(
            "NEWS2", totalScore, contributors, medication: medication);

        var inserted = await ClinicalAlertFactory.TryInsertAsync(
            db, alertId, encounterId, patientId,
            alertType, severity, details, explanation,
            observationCode: null, 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 = alertType.ToDbString(),
                severity = severity.ToDbString(),
                details,
                triggeredAt,
                news2Score = totalScore,
                news2RiskLevel = riskLevel,
                explanation,
                partitionKey = encounterId.ToString()
            }),
            PartitionKey = encounterId.ToString(),
            CreatedAt = DateTimeOffset.UtcNow
        });

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

        _metrics.ClinicalAlertsTotal
            .WithLabels(alertType.ToDbString(), severity.ToDbString()).Inc();

        _logger.LogWarning(
            "NEWS2 alert {AlertType} created for encounter {EncounterId} — score={Score} risk={Risk}",
            alertType.ToDbString(), encounterId, totalScore, riskLevel);

        return true;
    }

    private static string BuildDetails(int totalScore, string riskLevel, int[] paramScores)
    {
        var components = News2Calculator.ParameterCodes
            .Zip(paramScores, (code, score) => $"{code}={score}")
            .ToArray();
        return $"NEWS2 score {totalScore} ({riskLevel}): {string.Join(", ", components)}.";
    }
}