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

public class SofaDetector
{
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNameCaseInsensitive = true,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };

    private readonly IConnectionMultiplexer _redis;
    private readonly IServiceProvider _services;
    private readonly SofaLabCache _labCache;
    private readonly SofaVasopressorResolver _vasopressors;
    private readonly SofaOptions _options;
    private readonly ClinicalMetrics _metrics;
    private readonly ILogger<SofaDetector> _logger;

    public SofaDetector(
        IConnectionMultiplexer redis,
        IServiceProvider services,
        SofaLabCache labCache,
        SofaVasopressorResolver vasopressors,
        IOptions<SofaOptions> options,
        ClinicalMetrics metrics,
        ILogger<SofaDetector> logger)
    {
        _redis = redis;
        _services = services;
        _labCache = labCache;
        _vasopressors = vasopressors;
        _options = options.Value;
        _metrics = metrics;
        _logger = logger;
    }

    public async Task<SofaScoringResult> ProcessObservationAsync(
        Guid encounterId,
        Guid patientId,
        string observationCode,
        decimal value,
        DateTimeOffset recordedAt,
        CancellationToken ct = default)
    {
        if (!SofaCalculator.TriggersRescore(observationCode))
            return SofaScoringResult.NotSofaTrigger;

        if (SofaCalculator.IsSofaCode(observationCode))
        {
            await _labCache.StoreAsync(encounterId, observationCode, value, recordedAt);
        }

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

    public Task<SofaScoringResult> ProcessGcsScoredAsync(
        Guid encounterId, Guid patientId, CancellationToken ct = default) =>
        TryComputeScoreAsync(encounterId, patientId, ct);

    private async Task<SofaScoringResult> TryComputeScoreAsync(
        Guid encounterId, Guid patientId, CancellationToken ct)
    {
        using var timer = _metrics.SofaScoringDuration.NewTimer();

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

        var cached = await _labCache.GetAllAsync(encounterId);
        var staleComponents = new List<string>();
        var missingComponents = new List<string>();

        decimal? GetValue(string code)
        {
            if (!cached.TryGetValue(code, out var entry))
            {
                missingComponents.Add(code);
                return null;
            }
            var status = _labCache.Classify(entry);
            if (status == SofaValueStatus.Expired)
            {
                missingComponents.Add(code);
                return null;
            }
            if (status == SofaValueStatus.Stale)
                staleComponents.Add(code);
            return entry.Value;
        }

        var pao2 = GetValue("PAO2_MMHG");
        var fio2 = GetValue("FIO2_PCT");
        var spo2 = GetValue("SPO2");
        var supplementalO2 = GetValue("SUPPLEMENTAL_O2");
        var onMechanicalVent = supplementalO2 is >= 1m;

        var usedSpO2Fallback = false;
        int respiratory;
        if (pao2 is not null && fio2 is not null)
        {
            respiratory = SofaCalculator.ScoreRespiratory(pao2, fio2, onMechanicalVent);
        }
        else if (_options.UseSpO2FiO2Fallback && spo2 is not null && fio2 is not null)
        {
            usedSpO2Fallback = true;
            respiratory = SofaCalculator.ScoreRespiratoryFromSpo2(spo2, fio2, onMechanicalVent);
        }
        else
        {
            respiratory = 0;
            if (pao2 is null) missingComponents.Add("PAO2_MMHG");
            if (fio2 is null) missingComponents.Add("FIO2_PCT");
        }

        var coagulation = SofaCalculator.ScoreCoagulation(GetValue("PLATELET_K_UL"));
        var liver = SofaCalculator.ScoreLiver(GetValue("BILIRUBIN_MG_DL"));

        decimal? map = null;
        var sbp = GetValue("SYSTOLIC_BP");
        var dbp = GetValue("DIASTOLIC_BP");
        if (sbp is not null && dbp is not null)
            map = MapCalculator.Calculate(sbp.Value, dbp.Value);

        var vasopressor = await _vasopressors.GetActiveVasopressorAsync(encounterId, ct);
        var cardiovascular = SofaCalculator.ScoreCardiovascular(map, vasopressor);

        var gcsTotal = await LoadGcsTotalAsync(encounterId);
        var cns = SofaCalculator.ScoreCns(gcsTotal);

        var creatinine = GetValue("CREATININE_MG_DL");
        var urineMlH = GetValue("URINE_OUTPUT_ML_H");
        decimal? urineMlDay = urineMlH is not null ? urineMlH * 24m : null;
        var renal = SofaCalculator.ScoreRenal(creatinine, urineMlDay);

        var result = SofaCalculator.ComputeTotal(
            respiratory, coagulation, liver, cardiovascular, cns, renal);

        var rawValues = new Dictionary<string, string?>
        {
            ["RESPIRATORY"] = pao2 is not null && fio2 is not null
                ? $"PaO2/FiO2={pao2}/{fio2}%"
                : usedSpO2Fallback && spo2 is not null && fio2 is not null
                    ? $"SpO2/FiO2={spo2}/{fio2}%"
                    : null,
            ["COAGULATION"] = GetValue("PLATELET_K_UL")?.ToString(),
            ["LIVER"] = GetValue("BILIRUBIN_MG_DL")?.ToString(),
            ["CARDIOVASCULAR"] = vasopressor is not null
                ? $"{vasopressor.DrugName} {vasopressor.DoseUgKgMin} µg/kg/min"
                : map?.ToString(),
            ["CNS"] = gcsTotal?.ToString(),
            ["RENAL"] = creatinine?.ToString() ?? urineMlDay?.ToString()
        };
        var contributors = SofaContributorBuilder.Build(result, rawValues);

        var stalenessFlags = JsonSerializer.Serialize(new SofaStalenessFlags(
            staleComponents.Distinct().ToList(),
            missingComponents.Distinct().ToList(),
            usedSpO2Fallback), JsonOptions);

        var calculatedAt = DateTimeOffset.UtcNow;
        var (isBaseline, delta) = await ResolveBaselineAndDeltaAsync(
            encounterId, patientId, result, calculatedAt,
            SofaCalculator.CountPopulatedOrganSystems(
                hasRespiratory: (pao2 is not null && fio2 is not null)
                    || (_options.UseSpO2FiO2Fallback && spo2 is not null && fio2 is not null),
                hasCoagulation: cached.ContainsKey("PLATELET_K_UL")
                    && _labCache.Classify(cached["PLATELET_K_UL"]) != SofaValueStatus.Expired,
                hasLiver: cached.ContainsKey("BILIRUBIN_MG_DL")
                    && _labCache.Classify(cached["BILIRUBIN_MG_DL"]) != SofaValueStatus.Expired,
                hasCardiovascular: (sbp is not null && dbp is not null) || vasopressor is not null,
                hasCns: gcsTotal is not null,
                hasRenal: (cached.ContainsKey("CREATININE_MG_DL")
                        && _labCache.Classify(cached["CREATININE_MG_DL"]) != SofaValueStatus.Expired)
                    || (cached.ContainsKey("URINE_OUTPUT_ML_H")
                        && _labCache.Classify(cached["URINE_OUTPUT_ML_H"]) != SofaValueStatus.Expired)),
            ct);

        await PersistScoreAsync(
            encounterId, patientId, result, isBaseline, delta,
            stalenessFlags, calculatedAt, ct);

        var alertCreated = false;
        if (delta is >= 2)
        {
            alertCreated = await TryCreateAlertAsync(
                encounterId, patientId, AlertType.SofaSepsis, AlertSeverity.Critical,
                result, isBaseline ? null : delta, stalenessFlags, contributors, ct);
        }
        else if (delta == 1)
        {
            alertCreated = await TryCreateAlertAsync(
                encounterId, patientId, AlertType.SofaWarning, AlertSeverity.Warning,
                result, delta, stalenessFlags, contributors, ct);
        }

        _metrics.SofaScoresTotal
            .WithLabels(alertCreated ? "true" : "false")
            .Inc();

        _logger.LogInformation(
            "SOFA score {Total} for encounter {Id} — baseline={Baseline} delta={Delta}",
            result.Total, encounterId, isBaseline, delta);

        return new SofaScoringResult(
            SofaOutcome.ScoreComputed, result, isBaseline, delta, alertCreated, contributors);
    }

    private async Task<int?> LoadGcsTotalAsync(Guid encounterId)
    {
        var cache = _redis.GetDatabase();
        var gcsValues = await cache.StringGetAsync(GcsCalculator.AllComponentKeys(encounterId));
        if (!gcsValues.All(v => v.HasValue)) return null;

        var eye = decimal.Parse(gcsValues[0]!, CultureInfo.InvariantCulture);
        var verbal = decimal.Parse(gcsValues[1]!, CultureInfo.InvariantCulture);
        var motor = decimal.Parse(gcsValues[2]!, CultureInfo.InvariantCulture);
        return GcsCalculator.ComputeTotal(eye, verbal, motor);
    }

    private async Task<(bool IsBaseline, int? Delta)> ResolveBaselineAndDeltaAsync(
        Guid encounterId, Guid patientId, SofaResult result,
        DateTimeOffset calculatedAt, int populatedOrganSystems, CancellationToken ct)
    {
        using var scope = _services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        var existingBaseline = await db.SofaScores
            .AsNoTracking()
            .FirstOrDefaultAsync(s => s.EncounterId == encounterId && s.IsBaseline, ct);

        if (existingBaseline is null)
        {
            if (populatedOrganSystems >= 4)
                return (true, null);
            return (false, null);
        }

        var delta = result.Total - existingBaseline.TotalScore;
        return (false, delta);
    }

    private async Task PersistScoreAsync(
        Guid encounterId, Guid patientId, SofaResult result,
        bool isBaseline, int? delta, string stalenessFlags,
        DateTimeOffset calculatedAt, CancellationToken ct)
    {
        using var scope = _services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        db.SofaScores.Add(new SofaScore
        {
            Id = Guid.NewGuid(),
            EncounterId = encounterId,
            PatientId = patientId,
            TotalScore = result.Total,
            RespiratoryScore = result.Respiratory,
            CoagulationScore = result.Coagulation,
            LiverScore = result.Liver,
            CardiovascularScore = result.Cardiovascular,
            CnsScore = result.Cns,
            RenalScore = result.Renal,
            IsBaseline = isBaseline,
            DeltaFromBaseline = delta,
            StalenessFlags = stalenessFlags,
            CalculatedAt = calculatedAt,
            CreatedAt = DateTimeOffset.UtcNow
        });

        await db.SaveChangesAsync(ct);
    }

    private async Task<bool> TryCreateAlertAsync(
        Guid encounterId, Guid patientId,
        AlertType alertType, AlertSeverity severity,
        SofaResult result, int? delta, string stalenessFlags,
        List<ScoreContributor> contributors,
        CancellationToken ct)
    {
        if (alertType == AlertType.SofaWarning)
        {
            var suppression = _services.GetRequiredService<IAlertSuppressionService>();
            if (await suppression.IsSuppressedAsync(encounterId, alertType, ct))
                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 =
            $"SOFA score {result.Total} (delta +{delta}). " +
            $"Components: Resp={result.Respiratory}, Coag={result.Coagulation}, " +
            $"Liver={result.Liver}, CV={result.Cardiovascular}, CNS={result.Cns}, Renal={result.Renal}. " +
            $"Staleness: {stalenessFlags}";

        var explanation = AlertExplanationBuilder.Build(
            "SOFA", result.Total, contributors);

        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,
                sofaTotal = result.Total,
                sofaDelta = delta,
                explanation,
                partitionKey = encounterId.ToString()
            }),
            PartitionKey = encounterId.ToString(),
            CreatedAt = DateTimeOffset.UtcNow
        });

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

        if (alertType == AlertType.SofaSepsis)
        {
            var handler = _services.GetRequiredService<SepsisAlertHandler>();
            await handler.OnSepsisAlertCreatedAsync(encounterId, alertId, alertType, ct);
        }

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

        return true;
    }
}