namespace VigilCare.Simulation;

public class ReplayEngine
{
    private readonly VigilCareApiClient _client;
    private readonly IApiPoller? _poller;
    private readonly IReplayObserver _observer;
    private readonly Func<Guid, CancellationToken, Task>? _onPatientRegistered;
    private DateTimeOffset _scenarioStartTime;

    public ReplayEngine(
        VigilCareApiClient client,
        IApiPoller? poller,
        IReplayObserver? observer = null,
        Func<Guid, CancellationToken, Task>? onPatientRegistered = null)
    {
        _client = client;
        _poller = poller;
        _observer = observer ?? NullReplayObserver.Instance;
        _onPatientRegistered = onPatientRegistered;
    }

    public async Task<ReplayResult> RunAsync(
        ScenarioFile scenario, ReplayOptions options, CancellationToken ct = default)
    {
        var result = new ReplayResult(scenario.Scenario.Id);
        var startTime = DateTimeOffset.UtcNow;
        _scenarioStartTime = startTime;

        // --- Phase 1: Setup ---
        _observer.Header(scenario.Scenario.Name, scenario.Scenario.Description);

        if (options.DryRun)
        {
            if (options.ExistingEncounterId.HasValue)
            {
                result.EncounterId = options.ExistingEncounterId.Value;
                if (options.Target == ReplayTarget.Gateway)
                    _observer.DryRun($"Gateway mode — would use existing encounter {result.EncounterId}");
                else
                    _observer.DryRun($"Would use existing encounter {result.EncounterId}");
            }
            else
            {
                _observer.DryRun("Would register patient: " +
                    $"{scenario.Patient.FirstName} {scenario.Patient.LastName}");
                _observer.DryRun("Would open encounter: " +
                    $"{scenario.Encounter.Department} / {scenario.Encounter.EncounterType}");
            }
        }
        else if (options.Target == ReplayTarget.Gateway && options.ExistingEncounterId.HasValue)
        {
            result.EncounterId = options.ExistingEncounterId.Value;
            _observer.Info($"Gateway mode — using existing encounter {result.EncounterId}");
        }
        else if (options.ExistingEncounterId.HasValue)
        {
            result.EncounterId = options.ExistingEncounterId.Value;
            _observer.Info($"Using existing encounter {result.EncounterId}");
        }
        else
        {
            var patient = await _client.RegisterPatientAsync(new RegisterPatientRequest(
                scenario.Patient.FirstName,
                scenario.Patient.LastName,
                DateOnly.Parse(scenario.Patient.DateOfBirth),
                scenario.Patient.Gender));

            if (_onPatientRegistered is not null)
                await _onPatientRegistered(patient.Id, ct);

            var encounter = await _client.OpenEncounterAsync(patient.Id, new OpenEncounterRequest(
                scenario.Encounter.EncounterType,
                DepartmentMapper.ToApiDepartment(scenario.Encounter.Department),
                scenario.Encounter.AttendingPhysician,
                scenario.Encounter.RoomBed,
                scenario.Encounter.AdmissionReason));

            _observer.Info($"Patient registered: {patient.Id} ({patient.Mrn})");
            _observer.Info($"Encounter opened: {encounter.Id} ({encounter.Status})");
            result.PatientId = patient.Id;
            result.EncounterId = encounter.Id;
        }

        // --- Phase 2: Replay events ---
        double lastOffset = 0;
        var clusters = scenario.Events
            .GroupBy(e => e.OffsetMinutes)
            .OrderBy(g => g.Key)
            .ToList();

        for (var clusterIndex = 0; clusterIndex < clusters.Count; clusterIndex++)
        {
            var cluster = clusters[clusterIndex];
            ct.ThrowIfCancellationRequested();

            var deltaMinutes = cluster.Key - lastOffset;
            if (deltaMinutes > 0 && options.Speed > 0 && !options.DryRun)
            {
                var delayMs = (int)(deltaMinutes * 60_000 / options.Speed);
                _observer.Waiting(deltaMinutes, delayMs);
                await Task.Delay(delayMs, ct);
            }

            var simTimestamp = FormatSimTime(cluster.Key);
            var observationEvents = cluster.Where(e => e.Type == "observation").ToList();
            var otherEvents = cluster.Where(e => e.Type != "observation").ToList();

            if (observationEvents.Count > 0)
                await ReplayObservationCluster(observationEvents, simTimestamp, options, result);

            foreach (var evt in otherEvents)
            {
                switch (evt.Type)
                {
                    case "order":
                        await ReplayOrder(
                            evt, simTimestamp, options, result, scenario.Encounter.AttendingPhysician);
                        break;
                    case "medication":
                        await ReplayMedication(evt, simTimestamp, options, result);
                        break;
                    case "order_result":
                        await ReplayOrderResult(evt, simTimestamp, options, result);
                        break;
                    case "alert_ack":
                        await ReplayAlertAck(evt, simTimestamp, options, result, ct);
                        break;
                }
            }

            lastOffset = cluster.Key;
            _observer.Progress(cluster.Key, clusterIndex, clusters.Count);

            if (options.Poll && !options.DryRun && _poller is not null)
            {
                await Task.Delay(options.PollIntervalSeconds * 1000, ct);
                await _poller.PollAndDisplayAsync(result.EncounterId, simTimestamp);
            }
        }

        // --- Phase 3: Summary ---
        result.Duration = DateTimeOffset.UtcNow - startTime;

        if (!options.DryRun
            && options.Target == ReplayTarget.Central
            && scenario.ExpectedOutcomes is { Count: > 0 })
        {
            var failures = await ExpectedOutcomeValidator.ValidateAsync(
                _client, result.EncounterId, scenario, ct);
            result.HadExpectedOutcomes = true;
            result.OutcomeFailures.AddRange(failures);
            foreach (var failure in failures)
                _observer.Error(failure);
        }

        _observer.Completed(result);
        return result;
    }

    private async Task ReplayObservationCluster(
        List<ScenarioEvent> cluster, string simTime, ReplayOptions options,
        ReplayResult result)
    {
        var observations = new List<IngestObservationRequest>();
        var offsetMinutes = cluster[0].OffsetMinutes;
        var recordedAt = options.Target == ReplayTarget.Gateway
            ? DateTimeOffset.UtcNow
            : _scenarioStartTime.AddMinutes(offsetMinutes);

        foreach (var evt in cluster)
        {
            var code = evt.Data.GetProperty("code").GetString()!;
            var value = evt.Data.GetProperty("value").GetDecimal();
            var unit = evt.Data.GetProperty("unit").GetString()!;
            var source = evt.Data.TryGetProperty("source", out var s) ? s.GetString()! : "Manual";

            _observer.Event(simTime, $"{code} {value} {unit}");
            result.ObservationsSent++;

            if (!options.DryRun)
            {
                observations.Add(new IngestObservationRequest(
                    code, value, unit, ToApiSource(source), recordedAt, null));
            }
        }

        if (!options.DryRun && observations.Count > 0)
            await _client.SendObservationBatchAsync(result.EncounterId, observations, options.Target);
    }

    private async Task ReplayMedication(
        ScenarioEvent evt, string simTime, ReplayOptions options, ReplayResult result)
    {
        var drugName = evt.Data.GetProperty("drugName").GetString()!;
        var dose = evt.Data.GetProperty("dose").GetDecimal();
        var doseUnit = evt.Data.GetProperty("doseUnit").GetString()!;
        var route = evt.Data.GetProperty("route").GetString()!;
        var administeredBy = evt.Data.GetProperty("administeredBy").GetString()!;

        if (options.DryRun)
        {
            _observer.DryRun($"[{simTime}] MEDICATION {drugName} {dose}{doseUnit} {route}");
            return;
        }

        var sent = await _client.TrySendMedicationAsync(result.EncounterId,
            new CreateMedicationAdministrationRequest(drugName, dose, doseUnit, route, null, administeredBy));

        if (sent)
        {
            _observer.Event(simTime, $"MEDICATION {drugName} {dose}{doseUnit} ({route}) sent");
            result.MedicationsSent++;
        }
        else
        {
            _observer.Warn($"[{simTime}] MEDICATION skipped — endpoint not available (Phase 15 required)");
            result.MedicationsSkipped++;
        }
    }

    private async Task ReplayOrder(
        ScenarioEvent evt, string simTime, ReplayOptions options, ReplayResult result,
        string defaultOrderedBy)
    {
        var description = evt.Data.GetProperty("description").GetString()!;
        var orderType = evt.Data.GetProperty("orderType").GetString()!;
        var orderedBy = evt.Data.TryGetProperty("orderedBy", out var ob)
            ? ob.GetString()! : defaultOrderedBy;

        if (options.DryRun)
        {
            _observer.DryRun($"[{simTime}] ORDER {orderType}: {description}");
            return;
        }

        var placed = await _client.TryCreateOrderAsync(result.EncounterId,
            new CreateOrderRequest(orderType, description, orderedBy));

        if (placed)
        {
            _observer.Event(simTime, $"ORDER {description} placed");
            result.OrdersPlaced++;
        }
        else
        {
            _observer.Warn($"[{simTime}] ORDER skipped — failed to place '{description}'");
        }
    }

    private async Task ReplayOrderResult(
        ScenarioEvent evt, string simTime, ReplayOptions options, ReplayResult result)
    {
        var orderDesc = evt.Data.GetProperty("orderDescription").GetString()!;
        var resultSummary = evt.Data.TryGetProperty("resultSummary", out var rs)
            ? rs.GetString() : null;

        if (options.DryRun)
        {
            _observer.DryRun($"[{simTime}] ORDER_RESULT {orderDesc}");
            return;
        }

        var ok = await _client.TryResultOrderAsync(
            result.EncounterId, orderDesc, resultSummary);

        if (ok)
        {
            _observer.Event(simTime, $"ORDER_RESULT {orderDesc} → resulted");
            result.OrdersResulted++;
        }
        else
        {
            _observer.Warn($"[{simTime}] ORDER_RESULT skipped — order '{orderDesc}' not found or already resulted");
        }
    }

    private static string FormatSimTime(double offsetMinutes)
    {
        var hours = (int)(offsetMinutes / 60);
        var mins = (int)(offsetMinutes % 60);
        return $"{hours:D2}:{mins:D2}";
    }

    private static string ToApiSource(string source) => source switch
    {
        "Device" or "DEVICE" or "device" or "monitor" => "DEVICE",
        "Manual" or "MANUAL" or "manual" => "MANUAL",
        "Lab" or "LAB" or "lab" => "LAB",
        _ => throw new InvalidOperationException($"Unknown observation source: '{source}'")
    };

    private async Task ReplayAlertAck(
        ScenarioEvent evt, string simTime, ReplayOptions options, ReplayResult result,
        CancellationToken ct)
    {
        var alertType = evt.Data.GetProperty("alertType").GetString()!;
        var clinicianId = evt.Data.GetProperty("clinicianId").GetString()!;
        var note = evt.Data.TryGetProperty("note", out var n) ? n.GetString() : null;

        if (options.DryRun)
        {
            _observer.DryRun($"[{simTime}] ACK {alertType} by {clinicianId}");
            return;
        }

        var waitForAlert = options.Target == ReplayTarget.Central
            ? TimeSpan.FromSeconds(30)
            : (TimeSpan?)null;

        var ok = await _client.TryAcknowledgeAlertAsync(
            result.EncounterId, alertType, clinicianId, note, waitForAlert, ct);
        if (ok)
            _observer.Event(simTime, $"ACK {alertType} by {clinicianId}");
        else
            _observer.Warn($"[{simTime}] ACK failed — no open {alertType} alert found");
    }
}
