using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;

public class LiveCaptureService : ILiveCaptureService
{
    private readonly AppDbContext _db;
    private readonly IAttestationService _attestation;
    private readonly IConnectionMultiplexer _redis;
    private readonly ILogger<LiveCaptureService> _logger;

    // Redis key prefix for cached alert thresholds (same as VigilCareClinical)
    private const string ThresholdCachePrefix = "threshold:";

    public LiveCaptureService(
        AppDbContext db,
        IAttestationService attestation,
        IConnectionMultiplexer redis,
        ILogger<LiveCaptureService> logger)
    {
        _db = db;
        _attestation = attestation;
        _redis = redis;
        _logger = logger;
    }

    public async Task<LiveCaptureResponse> RecordObservationsAsync(
        Guid encounterId, RecordObservationsRequest request, Guid clinicianUserId)
    {
        // 1. Validate attestation (role + password re-confirm)
        var clinician = await _attestation.ValidateAttestationAsync(
            clinicianUserId, request.ClinicianAttestation, request.PasswordConfirm);

        // 2. Validate encounter exists and is active in VigilCareClinical
        var encounter = await _db.Encounters.FindAsync(encounterId);
        if (encounter is null)
            throw new NotFoundException(
                "Encounter not found in VigilCareClinical.", "ENCOUNTER_NOT_FOUND");

        if (encounter.Status != "active")
            throw new ConflictException(
                "Observations can only be recorded against active encounters.",
                "ENCOUNTER_NOT_ACTIVE");

        // 3. Validate observations
        if (request.Observations is null || request.Observations.Count == 0)
            throw new ValidationException(
                "At least one observation is required.", "EMPTY_OBSERVATIONS");

        if (request.Observations.Count > 10)
            throw new ValidationException(
                "Maximum 10 observations per live capture request.",
                "TOO_MANY_OBSERVATIONS");

        foreach (var obs in request.Observations)
        {
            if (!PlausibilityValidator.IsPlausible(obs.ObservationCode, obs.Value, out var reason))
                throw new ValidationException(reason!, "OBSERVATION_OUT_OF_PLAUSIBLE_RANGE");
        }

        // 4. Execute synchronous promotion within a single transaction
        return await PromoteSynchronouslyAsync(
            encounter.Id, encounter.PatientId, clinician, request.Observations);
    }

    public async Task<LiveCaptureResponse> OpenEncounterWithVitalsAsync(
        OpenEncounterWithVitalsRequest request, Guid clinicianUserId)
    {
        // 1. Validate attestation
        var clinician = await _attestation.ValidateAttestationAsync(
            clinicianUserId, request.ClinicianAttestation, request.PasswordConfirm);

        // 2. Validate patient exists
        var patient = await _db.Patients.FindAsync(request.PatientId);
        if (patient is null)
            throw new NotFoundException("Patient not found.", "PATIENT_NOT_FOUND");

        // 3. Validate observations
        if (request.Observations is null || request.Observations.Count == 0)
            throw new ValidationException(
                "At least one observation is required.", "EMPTY_OBSERVATIONS");

        if (request.Observations.Count > 10)
            throw new ValidationException(
                "Maximum 10 observations per live capture request.",
                "TOO_MANY_OBSERVATIONS");

        foreach (var obs in request.Observations)
        {
            if (!PlausibilityValidator.IsPlausible(obs.ObservationCode, obs.Value, out var reason))
                throw new ValidationException(reason!, "OBSERVATION_OUT_OF_PLAUSIBLE_RANGE");
        }

        // 4. Check no duplicate active encounter for this patient
        var existingActive = await _db.Encounters.AnyAsync(e =>
            e.PatientId == request.PatientId &&
            e.Status == "active");

        if (existingActive)
            throw new ConflictException(
                "Patient already has an active encounter. Record observations against the existing encounter.",
                "ACTIVE_ENCOUNTER_EXISTS");

        if (!DepartmentExtensions.TryFromDbString(request.Department, out var department))
            throw new ValidationException(
                $"Invalid department '{request.Department}'. Must be a recognized hospital department.",
                "INVALID_DEPARTMENT");

        // 5. Create the encounter in VigilCareClinical
        var encounter = new Encounter
        {
            Id = Guid.NewGuid(),
            PatientId = request.PatientId,
            Department = department,
            RoomBed = request.RoomBed,
            AdmissionReason = request.AdmissionReason,
            Status = "active",
            AdmissionDate = DateTimeOffset.UtcNow,
            CreatedAt = DateTimeOffset.UtcNow,
            UpdatedAt = DateTimeOffset.UtcNow
        };

        _db.Encounters.Add(encounter);

        // 6. Promote observations synchronously
        return await PromoteSynchronouslyAsync(
            encounter.Id, request.PatientId, clinician, request.Observations);
    }

    /// <summary>
    /// Core promotion logic shared by both endpoints. Creates the batch, draft
    /// observations, live observations, evaluates critical thresholds, and writes
    /// all audit and outbox events within a single database transaction.
    /// </summary>
    private async Task<LiveCaptureResponse> PromoteSynchronouslyAsync(
        Guid encounterId, Guid patientId, User clinician,
        List<LiveCaptureObservationRequest> observations)
    {
        var now = DateTimeOffset.UtcNow;
        var batchId = Guid.NewGuid();
        var observationResponses = new List<LiveCaptureObservationResponse>();
        var criticalAlertCount = 0;

        await using var transaction = await _db.Database.BeginTransactionAsync();

        try
        {
            // --- Create the live_capture batch (already in terminal Promoted state) ---
            var batch = new DigitizationBatch
            {
                Id = batchId,
                Status = BatchStatus.Promoted,
                BatchType = BatchType.VitalsSheet,
                Track = BatchTrack.LiveCapture,
                PatientId = patientId,
                DocumentRef = "live-capture", // No scanned document for Track B
                DocumentSha256 = ComputeLiveCaptureHash(clinician.Id, encounterId, now),
                EnableRetroactiveAlerts = false, // Not applicable — live capture always alerts
                EnteredByUserId = clinician.Id,
                VerifiedByUserId = clinician.Id, // Clinician attestation replaces verifier
                ApprovedByUserId = clinician.Id,
                ClinicianAttestation = true,
                PromotedAt = now,
                PromotionEncounterId = encounterId,
                CreatedAt = now,
                UpdatedAt = now
            };

            _db.DigitizationBatches.Add(batch);

            // --- Write attestation and promotion events ---
            _db.DigitizationEvents.Add(new DigitizationEvent
            {
                Id = Guid.NewGuid(),
                BatchId = batchId,
                EventType = DigitizationEventType.LiveCaptureAttested,
                ActorUserId = clinician.Id,
                OccurredAt = now,
                MetadataJson = JsonSerializer.Serialize(new
                {
                    clinicianId = clinician.Id,
                    clinicianName = clinician.FullName,
                    encounterId,
                    observationCount = observations.Count,
                    track = "LIVE_CAPTURE"
                })
            });

            _db.DigitizationEvents.Add(new DigitizationEvent
            {
                Id = Guid.NewGuid(),
                BatchId = batchId,
                EventType = DigitizationEventType.Promoted,
                ActorUserId = clinician.Id,
                OccurredAt = now.AddMilliseconds(1),
                MetadataJson = JsonSerializer.Serialize(new
                {
                    promotionType = "synchronous_live_capture",
                    encounterId
                })
            });

            // --- Process each observation ---
            var cache = _redis.GetDatabase();

            foreach (var obs in observations)
            {
                var draftObsId = Guid.NewGuid();
                var liveObsId = Guid.NewGuid();

                // Create draft observation (audit trail)
                var draftObservation = new DraftObservation
                {
                    Id = draftObsId,
                    BatchId = batchId,
                    ObservationCode = obs.ObservationCode,
                    Value = obs.Value,
                    Unit = obs.Unit,
                    RecordedAt = obs.RecordedAt,
                    Note = obs.Note,
                    CreatedAt = now
                };

                _db.DraftObservations.Add(draftObservation);

                // Create live observation in VigilCareClinical tables
                var liveObservation = new Observation
                {
                    Id = liveObsId,
                    EncounterId = encounterId,
                    PatientId = patientId,
                    ObservationCode = obs.ObservationCode,
                    Value = obs.Value,
                    Unit = obs.Unit,
                    RecordedAt = obs.RecordedAt,
                    Note = obs.Note,
                    Source = "live_capture",
                    SourceDraftObservationId = draftObsId,
                    SourceBatchId = batchId,
                    CreatedAt = now
                };

                _db.Observations.Add(liveObservation);

                _db.OutboxEvents.Add(new OutboxEvent
                {
                    Id = Guid.NewGuid(),
                    EventType = "observation.recorded",
                    AggregateType = "Observation",
                    AggregateId = liveObsId,
                    PayloadJson = JsonSerializer.Serialize(new
                    {
                        observationId = liveObsId,
                        encounterId,
                        patientId,
                        observationCode = obs.ObservationCode,
                        value = obs.Value,
                        unit = obs.Unit,
                        recordedAt = obs.RecordedAt,
                        source = "live_capture",
                        batchId
                    }),
                    CreatedAt = now,
                    ProcessedAt = null,
                    RetryCount = 0
                });

                // --- Synchronous critical value detection ---
                var alert = await EvaluateCriticalThresholdAsync(
                    cache, liveObsId, encounterId, patientId,
                    obs.ObservationCode, obs.Value, obs.Unit, now);

                if (alert is not null)
                {
                    criticalAlertCount++;

                    observationResponses.Add(new LiveCaptureObservationResponse(
                        draftObsId, liveObsId,
                        obs.ObservationCode, obs.Value, obs.Unit, obs.RecordedAt,
                        new LiveCaptureCriticalAlert(
                            alert.Id,
                            alert.Severity,
                            alert.Message,
                            alert.ThresholdValue,
                            alert.ThresholdBound)));
                }
                else
                {
                    observationResponses.Add(new LiveCaptureObservationResponse(
                        draftObsId, liveObsId,
                        obs.ObservationCode, obs.Value, obs.Unit, obs.RecordedAt,
                        null));
                }
            }

            await _db.SaveChangesAsync();
            await transaction.CommitAsync();

            _logger.LogInformation(
                "Live capture batch {BatchId} promoted synchronously: " +
                "{ObsCount} observations, {AlertCount} critical alerts, " +
                "encounter {EncounterId}, clinician {ClinicianId}",
                batchId, observations.Count, criticalAlertCount,
                encounterId, clinician.Id);

            return new LiveCaptureResponse(
                batchId, encounterId, observationResponses,
                criticalAlertCount, now);
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }

    /// <summary>
    /// Evaluates a single observation against Redis-cached alert thresholds.
    /// If the value breaches a CRITICAL bound, creates a ClinicalAlert row and
    /// an outbox event within the current transaction (before SaveChanges).
    ///
    /// Returns null if no critical threshold is breached.
    /// Warning thresholds are handled asynchronously by the Kafka consumer —
    /// the same split as VigilCareClinical Phase 2.
    /// </summary>
    private async Task<CriticalAlertResult?> EvaluateCriticalThresholdAsync(
        StackExchange.Redis.IDatabase cache, Guid observationId, Guid encounterId, Guid patientId,
        string observationCode, decimal value, string unit, DateTimeOffset now)
    {
        var threshold = await LoadThresholdAsync(cache, observationCode);
        if (threshold is null)
            return null;

        var breach = GetCriticalBreach(value, threshold);
        if (breach is null)
            return null;

        var (thresholdValue, thresholdBound) = breach.Value;
        var details = BuildCriticalDetails(observationCode, value, unit, threshold, thresholdBound);

        AlertType alertType;
        try
        {
            alertType = AlertTypeExtensions.CriticalFor(observationCode);
        }
        catch (ArgumentOutOfRangeException)
        {
            _logger.LogWarning(
                "No critical alert type configured for observation code {ObservationCode}",
                observationCode);
            return null;
        }

        var alertId = Guid.NewGuid();
        var clinicalAlert = new ClinicalAlert
        {
            Id = alertId,
            EncounterId = encounterId,
            PatientId = patientId,
            ObservationId = observationId,
            ObservationCode = observationCode,
            AlertType = alertType,
            Severity = AlertSeverity.Critical,
            Details = details,
            Status = AlertStatus.Open,
            TriggeredAt = now
        };

        _db.ClinicalAlerts.Add(clinicalAlert);

        _db.OutboxEvents.Add(new OutboxEvent
        {
            Id = Guid.NewGuid(),
            EventType = "alert.generated",
            AggregateType = "ClinicalAlert",
            AggregateId = alertId,
            PayloadJson = JsonSerializer.Serialize(new
            {
                alertId,
                encounterId,
                patientId,
                observationId,
                observationCode,
                alertType = alertType.ToDbString(),
                severity = AlertSeverity.Critical.ToDbString(),
                details,
                triggeredValue = value,
                thresholdValue,
                thresholdBound,
                source = "live_capture",
                triggeredAt = now
            }),
            CreatedAt = now,
            ProcessedAt = null,
            RetryCount = 0
        });

        _logger.LogWarning(
            "CRITICAL alert {AlertId} generated via live capture: " +
            "{ObservationCode} = {Value} {Unit} ({ThresholdBound} = {ThresholdValue}), " +
            "encounter {EncounterId}, patient {PatientId}",
            alertId, observationCode, value, unit,
            thresholdBound, thresholdValue, encounterId, patientId);

        return new CriticalAlertResult(
            alertId,
            AlertSeverity.Critical.ToDbString(),
            details,
            thresholdValue,
            thresholdBound);
    }

    private async Task<ThresholdCacheEntry?> LoadThresholdAsync(
        StackExchange.Redis.IDatabase cache, string observationCode)
    {
        var cacheKey = $"{ThresholdCachePrefix}{observationCode}";
        var cached = await cache.StringGetAsync(cacheKey);
        if (cached.HasValue)
            return JsonSerializer.Deserialize<ThresholdCacheEntry>(cached!);

        var threshold = await _db.AlertThresholds
            .AsNoTracking()
            .FirstOrDefaultAsync(t => t.ObservationCode == observationCode);

        if (threshold is null)
            return null;

        var entry = new ThresholdCacheEntry(
            threshold.ObservationCode,
            threshold.CriticalLow,
            threshold.WarningLow,
            threshold.WarningHigh,
            threshold.CriticalHigh);

        await cache.StringSetAsync(
            cacheKey,
            JsonSerializer.Serialize(entry),
            TimeSpan.FromMinutes(30));

        return entry;
    }

    private static (decimal ThresholdValue, string ThresholdBound)? GetCriticalBreach(
        decimal value, ThresholdCacheEntry threshold)
    {
        if (threshold.CriticalLow.HasValue && value < threshold.CriticalLow.Value)
            return (threshold.CriticalLow.Value, "CRITICAL_LOW");

        if (threshold.CriticalHigh.HasValue && value > threshold.CriticalHigh.Value)
            return (threshold.CriticalHigh.Value, "CRITICAL_HIGH");

        return null;
    }

    private static string BuildCriticalDetails(
        string observationCode, decimal value, string unit,
        ThresholdCacheEntry threshold, string thresholdBound)
    {
        if (thresholdBound == "CRITICAL_LOW")
        {
            return $"{observationCode} value {value} {unit} is below critical low " +
                   $"threshold of {threshold.CriticalLow} {unit}";
        }

        return $"{observationCode} value {value} {unit} is above critical high " +
               $"threshold of {threshold.CriticalHigh} {unit}";
    }

    /// <summary>
    /// Computes a deterministic hash for live capture batches (no scanned document).
    /// Uses clinician ID, encounter ID, and timestamp to generate uniqueness.
    /// </summary>
    private static string ComputeLiveCaptureHash(Guid clinicianId, Guid encounterId, DateTimeOffset timestamp)
    {
        var input = $"{clinicianId}:{encounterId}:{timestamp:O}";
        var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
        return Convert.ToHexString(bytes).ToLowerInvariant();
    }
}