using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;

public static class ExplainableAlertsTestHelper
{
    public static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNameCaseInsensitive = true,
        Converters =
        {
            // Must match API Program.cs converters — AlertType/Severity/Status
            // serialize as DB strings (e.g. "GCS_CRITICAL"), not enum names.
            new AlertTypeJsonConverter(),
            new AlertSeverityJsonConverter(),
            new AlertStatusJsonConverter(),
            new JsonStringEnumConverter()
        }
    };

    private static readonly DateTimeOffset TrendBaseTime =
        new(2026, 6, 18, 10, 0, 0, TimeSpan.Zero);

    public static async Task ResetAsync(ApiFixture fixture)
    {
        using var scope = fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        await DbResetHelper.ResetAsync(db);

        var redis = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>();
        var server = redis.GetServer(redis.GetEndPoints().First());
        await server.FlushDatabaseAsync(1);
    }

    public static async Task<(Guid EncounterId, Guid PatientId)> SeedEncounterAsync(ApiFixture fixture)
    {
        using var scope = fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        var patient = new Patient
        {
            Id = Guid.NewGuid(), Mrn = $"MRN-EXP-{Guid.NewGuid():N}"[..16],
            FirstName = "Explain", LastName = "Test",
            DateOfBirth = new DateOnly(1970, 1, 1), Gender = "M",
            CreatedAt = DateTimeOffset.UtcNow
        };
        var encounter = new Encounter
        {
            Id = Guid.NewGuid(), PatientId = patient.Id, EncounterType = EncounterType.Inpatient,
            Status = EncounterStatus.Active, Department = Department.Icu,
            AttendingPhysician = "Dr. Explain", AdmittedAt = DateTimeOffset.UtcNow,
            CreatedAt = DateTimeOffset.UtcNow
        };
        db.Patients.Add(patient);
        db.Encounters.Add(encounter);
        await db.SaveChangesAsync();

        return (encounter.Id, patient.Id);
    }

    public static async Task FeedNews2VitalsAsync(
        ApiFixture fixture, Guid encounterId, Guid patientId,
        int respRate, decimal spo2, decimal systolicBp, decimal heartRate,
        decimal tempC, decimal supplementalO2, decimal avpu = 0m)
    {
        await ClearNews2KeysAsync(fixture, encounterId);

        using var scope = fixture.Services.CreateScope();
        var detector = scope.ServiceProvider.GetRequiredService<News2Detector>();

        await detector.ProcessObservationAsync(encounterId, patientId, "RESP_RATE", respRate);
        await detector.ProcessObservationAsync(encounterId, patientId, "SPO2", spo2);
        await detector.ProcessObservationAsync(encounterId, patientId, "SYSTOLIC_BP", systolicBp);
        await detector.ProcessObservationAsync(encounterId, patientId, "HEART_RATE", heartRate);
        await detector.ProcessObservationAsync(encounterId, patientId, "AVPU", avpu);
        await detector.ProcessObservationAsync(encounterId, patientId, "TEMP_C", tempC);
        await detector.ProcessObservationAsync(encounterId, patientId, "SUPPLEMENTAL_O2", supplementalO2);
    }

    public static async Task<(Guid EncounterId, Guid PatientId)> SeedEncounterWithVitalsAsync(
        ApiFixture fixture,
        int respRate, decimal spo2, decimal systolicBp, decimal heartRate,
        decimal tempC, decimal supplementalO2, decimal avpu = 0m)
    {
        var (encounterId, patientId) = await SeedEncounterAsync(fixture);
        await FeedNews2VitalsAsync(
            fixture, encounterId, patientId, respRate, spo2, systolicBp, heartRate,
            tempC, supplementalO2, avpu);
        return (encounterId, patientId);
    }

    public static async Task<(Guid EncounterId, Guid PatientId)> SeedSofaBaselineAsync(ApiFixture fixture)
    {
        var (encounterId, patientId) = await SeedEncounterAsync(fixture);
        await SeedSofaBaselineInputsAsync(fixture, encounterId, patientId);
        return (encounterId, patientId);
    }

    public static async Task AdvanceSofaDeltaAsync(
        ApiFixture fixture, Guid encounterId, Guid patientId, int delta = 2)
    {
        using var scope = fixture.Services.CreateScope();
        var detector = scope.ServiceProvider.GetRequiredService<SofaDetector>();

        if (delta >= 2)
        {
            await detector.ProcessObservationAsync(encounterId, patientId, "PLATELET_K_UL", 20m, DateTimeOffset.UtcNow);
            await detector.ProcessObservationAsync(encounterId, patientId, "CREATININE_MG_DL", 4.5m, DateTimeOffset.UtcNow);
        }
        else
        {
            await detector.ProcessObservationAsync(encounterId, patientId, "PLATELET_K_UL", 120m, DateTimeOffset.UtcNow);
        }
    }

    public static async Task RecordGcsAsync(
        ApiFixture fixture, Guid encounterId, Guid patientId, int eye, int verbal, int motor)
    {
        using var scope = fixture.Services.CreateScope();
        var detector = scope.ServiceProvider.GetRequiredService<GcsDetector>();
        await ClearGcsKeysAsync(fixture, encounterId);

        await detector.ProcessObservationAsync(encounterId, patientId, "GCS_EYE", eye);
        await detector.ProcessObservationAsync(encounterId, patientId, "GCS_VERBAL", verbal);
        await detector.ProcessObservationAsync(encounterId, patientId, "GCS_MOTOR", motor);
    }

    public static async Task SimulateRapidRespRateRiseAsync(
        ApiFixture fixture, Guid encounterId, Guid patientId)
    {
        using var scope = fixture.Services.CreateScope();
        var detector = scope.ServiceProvider.GetRequiredService<TrendDetector>();
        await ClearTrendKeysAsync(fixture, encounterId);

        await detector.ProcessObservationAsync(
            encounterId, patientId, "RESP_RATE", 14m, TrendBaseTime);
        await detector.ProcessObservationAsync(
            encounterId, patientId, "RESP_RATE", 28m, TrendBaseTime.AddMinutes(10));
    }

    public static async Task AdministerMedicationAsync(
        ApiFixture fixture, Guid encounterId, string drugName, decimal dose, string unit)
    {
        using var scope = fixture.Services.CreateScope();
        var medService = scope.ServiceProvider.GetRequiredService<IMedicationService>();
        await medService.CreateAsync(encounterId,
            new CreateMedicationAdministrationRequest(
                drugName, dose, unit, "PO",
                DateTimeOffset.UtcNow.AddMinutes(-30), "nurse-test"));
    }

    public static async Task RecordVitalAsync(
        ApiFixture fixture, Guid encounterId, Guid patientId, string code, decimal value)
    {
        using var scope = fixture.Services.CreateScope();
        var detector = scope.ServiceProvider.GetRequiredService<News2Detector>();
        await detector.ProcessObservationAsync(encounterId, patientId, code, value);
    }

    public static async Task<ClinicalAlert> WaitForAlertAsync(
        ApiFixture fixture, Guid encounterId, AlertType alertType)
    {
        using var scope = fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var alert = await db.ClinicalAlerts
            .AsNoTracking()
            .Where(a => a.EncounterId == encounterId && a.AlertType == alertType)
            .OrderByDescending(a => a.TriggeredAt)
            .FirstOrDefaultAsync();

        if (alert is null)
            throw new InvalidOperationException(
                $"Expected {alertType} alert on encounter {encounterId} but none was found.");

        return alert;
    }

    public static async Task<T> GetAlertAsync<T>(ApiFixture fixture, Guid alertId)
    {
        using var client = fixture.CreateClient();
        var response = await client.GetAsync($"/api/v1/alerts/{alertId}");
        response.EnsureSuccessStatusCode();
        var body = await response.Content.ReadFromJsonAsync<ApiResponse<T>>(JsonOptions);
        return body!.Data!;
    }

    public static async Task<AlertResponse> WaitForFirstAlertWithExplanationAsync(
        ApiFixture fixture, Guid encounterId)
    {
        using var scope = fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var alert = await db.ClinicalAlerts
            .AsNoTracking()
            .Where(a => a.EncounterId == encounterId && a.Explanation != null)
            .OrderByDescending(a => a.TriggeredAt)
            .FirstOrDefaultAsync();

        if (alert is null)
            throw new InvalidOperationException(
                $"Expected an alert with explanation on encounter {encounterId}.");

        return await GetAlertAsync<AlertResponse>(fixture, alert.Id);
    }

    public static async Task<AlertResponse> GetAnyAlertWithExplanationAsync(ApiFixture fixture)
    {
        await SeedEncounterWithVitalsAsync(
            fixture, respRate: 25, spo2: 91, systolicBp: 95, heartRate: 72,
            tempC: 37.0m, supplementalO2: 0m);
        using var scope = fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var alert = await db.ClinicalAlerts
            .AsNoTracking()
            .Where(a => a.Explanation != null)
            .OrderByDescending(a => a.TriggeredAt)
            .FirstAsync();

        return await GetAlertAsync<AlertResponse>(fixture, alert.Id);
    }

    public static async Task<Guid> SeedLegacyAlertWithoutExplanationAsync(ApiFixture fixture)
    {
        var (encounterId, patientId) = await SeedEncounterAsync(fixture);
        using var scope = fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var alertId = Guid.NewGuid();
        db.ClinicalAlerts.Add(new ClinicalAlert
        {
            Id = alertId,
            EncounterId = encounterId,
            PatientId = patientId,
            AlertType = AlertType.CriticalPotassiumMeqL,
            Severity = AlertSeverity.Critical,
            Details = "Potassium 2.1 mEq/L is below critical low.",
            Status = AlertStatus.Open,
            TriggeredAt = DateTimeOffset.UtcNow.AddDays(-1),
            Explanation = null
        });
        await db.SaveChangesAsync();
        return alertId;
    }

    public static async Task<News2ReplayResult> RunNews2HighScoreReplayAsync(ApiFixture fixture)
    {
        var (encounterId, patientId) = await SeedEncounterWithVitalsAsync(
            fixture, respRate: 25, spo2: 91, systolicBp: 95, heartRate: 72,
            tempC: 37.0m, supplementalO2: 0m);

        using var scope = fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var score = await db.News2Scores
            .Where(s => s.EncounterId == encounterId)
            .OrderByDescending(s => s.CalculatedAt)
            .FirstAsync();
        var alert = await db.ClinicalAlerts
            .Where(a => a.EncounterId == encounterId)
            .OrderByDescending(a => a.TriggeredAt)
            .FirstAsync();

        return new News2ReplayResult(score.TotalScore, alert.AlertType);
    }

    private static async Task SeedSofaBaselineInputsAsync(
        ApiFixture fixture, Guid encounterId, Guid patientId)
    {
        using var scope = fixture.Services.CreateScope();
        var detector = scope.ServiceProvider.GetRequiredService<SofaDetector>();

        await detector.ProcessObservationAsync(encounterId, patientId, "PAO2_MMHG", 100m, DateTimeOffset.UtcNow);
        await detector.ProcessObservationAsync(encounterId, patientId, "FIO2_PCT", 40m, DateTimeOffset.UtcNow);
        await detector.ProcessObservationAsync(encounterId, patientId, "PLATELET_K_UL", 180m, DateTimeOffset.UtcNow);
        await detector.ProcessObservationAsync(encounterId, patientId, "BILIRUBIN_MG_DL", 1.0m, DateTimeOffset.UtcNow);
        await detector.ProcessObservationAsync(encounterId, patientId, "SYSTOLIC_BP", 120m, DateTimeOffset.UtcNow);
        await detector.ProcessObservationAsync(encounterId, patientId, "DIASTOLIC_BP", 80m, DateTimeOffset.UtcNow);
        await detector.ProcessObservationAsync(encounterId, patientId, "CREATININE_MG_DL", 1.0m, DateTimeOffset.UtcNow);
        await detector.ProcessObservationAsync(encounterId, patientId, "URINE_OUTPUT_ML_H", 50m, DateTimeOffset.UtcNow);
    }

    private static async Task ClearNews2KeysAsync(ApiFixture fixture, Guid encounterId)
    {
        using var scope = fixture.Services.CreateScope();
        var cache = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>().GetDatabase();
        foreach (var key in News2Calculator.AllParameterKeys(encounterId))
            await cache.KeyDeleteAsync(key);
    }

    private static async Task ClearGcsKeysAsync(ApiFixture fixture, Guid encounterId)
    {
        using var scope = fixture.Services.CreateScope();
        var cache = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>().GetDatabase();
        foreach (var key in GcsCalculator.AllComponentKeys(encounterId))
            await cache.KeyDeleteAsync(key);
    }

    private static async Task ClearTrendKeysAsync(ApiFixture fixture, Guid encounterId)
    {
        using var scope = fixture.Services.CreateScope();
        var cache = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>().GetDatabase();
        foreach (var key in TrendCalculator.AllHistoryKeys(encounterId))
            await cache.KeyDeleteAsync(key);
    }

    public record News2ReplayResult(int TotalScore, AlertType AlertType);
}
