using System.Text.Json;
using Microsoft.EntityFrameworkCore;

public class EncounterService : IEncounterService
{
     // Explicit transition matrix. Every allowed move is listed here.
    // Any transition not in this dictionary is illegal and throws ConflictException.
    private static readonly Dictionary<EncounterStatus, HashSet<EncounterStatus>> _allowedTransitions = new()
    {
        [EncounterStatus.Scheduled] = new() { EncounterStatus.Active, EncounterStatus.Cancelled },
        [EncounterStatus.Active]    = new() { EncounterStatus.Discharged, EncounterStatus.Cancelled },
        [EncounterStatus.Discharged] = new(),
        [EncounterStatus.Cancelled]  = new(),
    };

    private readonly AppDbContext _db;
    private readonly IQsofaService _qsofa;
    private readonly IExternalIdentifierService _identifiers;
    private readonly IAuditService _audit;

    public EncounterService(
        AppDbContext db,
        IQsofaService qsofa,
        IExternalIdentifierService identifiers,
        IAuditService audit)
    {
        _db = db;
        _qsofa = qsofa;
        _identifiers = identifiers;
        _audit = audit;
    }

    public async Task<Encounter> GetByIdAsync(Guid id)
    {
        var encounter = await _db.Encounters
            .Include(e => e.Patient)
            .Include(e => e.Observations.OrderByDescending(o => o.RecordedAt).Take(10))
            .Include(e => e.Alerts.Where(a => a.Status == AlertStatus.Open))
            .FirstOrDefaultAsync(e => e.Id == id);

        if (encounter is null)
            throw new NotFoundException("Encounter not found.", "ENCOUNTER_NOT_FOUND");

        return encounter;
    }

    public async Task<PagedResult<WardEncounterSummary>> ListAsync(
        EncounterStatus? status, Department? department, int page, int pageSize)
    {
        page = Math.Max(1, page);
        pageSize = Math.Clamp(pageSize, 1, 100);

        var query = _db.Encounters
            .AsNoTracking()
            .Include(e => e.Patient)
            .AsQueryable();

        if (status.HasValue)
            query = query.Where(e => e.Status == status.Value);

        if (department.HasValue)
            query = query.Where(e => e.Department == department.Value);

        var total = await query.CountAsync();

        var encounters = await query
            .OrderByDescending(e => e.AdmittedAt)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();

        if (encounters.Count == 0)
            return new PagedResult<WardEncounterSummary>([], page, pageSize, total);

        var encounterIds = encounters.Select(e => e.Id).ToList();

        var news2ByEncounter = (await _db.News2Scores
            .AsNoTracking()
            .Where(s => encounterIds.Contains(s.EncounterId))
            .OrderByDescending(s => s.CalculatedAt)
            .ToListAsync())
            .GroupBy(s => s.EncounterId)
            .ToDictionary(g => g.Key, g => g.First());

        var openAlertCounts = await _db.ClinicalAlerts
            .AsNoTracking()
            .Where(a => encounterIds.Contains(a.EncounterId) && a.Status == AlertStatus.Open)
            .GroupBy(a => a.EncounterId)
            .Select(g => new { EncounterId = g.Key, Count = g.Count() })
            .ToDictionaryAsync(x => x.EncounterId, x => x.Count);

        var bundlesByEncounter = (await _db.SepsisBundles
            .AsNoTracking()
            .Where(b => encounterIds.Contains(b.EncounterId))
            .OrderByDescending(b => b.RecognizedAt)
            .ToListAsync())
            .GroupBy(b => b.EncounterId)
            .ToDictionary(g => g.Key, g => g.First());

        var sofaByEncounter = (await _db.SofaScores
            .AsNoTracking()
            .Where(s => encounterIds.Contains(s.EncounterId))
            .OrderByDescending(s => s.CalculatedAt)
            .ToListAsync())
            .GroupBy(s => s.EncounterId)
            .ToDictionary(g => g.Key, g => g.First());

        var gcsByEncounter = (await _db.GcsScores
            .AsNoTracking()
            .Where(s => encounterIds.Contains(s.EncounterId))
            .OrderByDescending(s => s.CalculatedAt)
            .ToListAsync())
            .GroupBy(s => s.EncounterId)
            .ToDictionary(g => g.Key, g => g.First());

        var lastObservationByEncounter = await _db.Observations
            .AsNoTracking()
            .Where(o => encounterIds.Contains(o.EncounterId))
            .GroupBy(o => o.EncounterId)
            .Select(g => new { EncounterId = g.Key, LastAt = g.Max(o => o.RecordedAt) })
            .ToDictionaryAsync(x => x.EncounterId, x => x.LastAt);

        var summaries = new List<WardEncounterSummary>(encounters.Count);
        foreach (var encounter in encounters)
        {
            news2ByEncounter.TryGetValue(encounter.Id, out var news2);
            bundlesByEncounter.TryGetValue(encounter.Id, out var bundle);
            openAlertCounts.TryGetValue(encounter.Id, out var openCount);
            sofaByEncounter.TryGetValue(encounter.Id, out var sofa);
            gcsByEncounter.TryGetValue(encounter.Id, out var gcs);
            lastObservationByEncounter.TryGetValue(encounter.Id, out var lastObservationAt);

            var qsofaScore = await _qsofa.GetActiveCriteriaCountAsync(encounter.Id);

            summaries.Add(new WardEncounterSummary(
                encounter.Id,
                encounter.PatientId,
                encounter.Patient.Mrn,
                encounter.Patient.FirstName,
                encounter.Patient.LastName,
                encounter.RoomBed,
                encounter.Department,
                encounter.Status,
                news2?.TotalScore,
                news2?.RiskLevel,
                qsofaScore,
                bundle is not null && bundle.ComplianceStatus != SepsisBundleComplianceStatus.Compliant,
                bundle?.ComplianceStatus,
                openCount,
                sofa?.TotalScore,
                sofa?.DeltaFromBaseline,
                gcs?.TotalScore,
                gcs?.Classification,
                lastObservationAt,
                encounter.AttendingPhysician,
                encounter.AdmittedAt));
        }

        return new PagedResult<WardEncounterSummary>(summaries, page, pageSize, total);
    }

    public async Task<EncounterStatusTransitionResult> TransitionStatusAsync(
        Guid encounterId, EncounterStatus targetStatus, string? dischargeDiagnosis = null)
    {
        var encounter = await _db.Encounters
            .Include(e => e.Patient)
            .FirstOrDefaultAsync(e => e.Id == encounterId);

        if (encounter is null)
            throw new NotFoundException("Encounter not found.", "ENCOUNTER_NOT_FOUND");

        if (!_allowedTransitions[encounter.Status].Contains(targetStatus))
            throw new ConflictException(
                $"Transition to '{targetStatus}' is not permitted from the current status.",
                "ILLEGAL_STATUS_TRANSITION");
        
        var previousStatus = encounter.Status;
        encounter.Status = targetStatus;

        if (targetStatus == EncounterStatus.Discharged)
        {
            encounter.DischargedAt = DateTimeOffset.UtcNow;
            encounter.DischargeDiagnosis = dischargeDiagnosis;
        }

        _db.OutboxEvents.Add(new OutboxEvent
        {
            Id = Guid.NewGuid(),
            Topic = "encounter.status.changed",
            Payload = JsonSerializer.Serialize(new
            {
                encounterId,
                patientId = encounter.PatientId,
                mrn = encounter.Patient.Mrn,
                patientName = $"{encounter.Patient.FirstName} {encounter.Patient.LastName}",
                previousStatus = previousStatus.ToDbString(),
                newStatus = targetStatus.ToDbString(),
                department = encounter.Department.ToDbString(),
                attendingPhysician = encounter.AttendingPhysician,
                roomBed = encounter.RoomBed,
                admissionReason = encounter.AdmissionReason,
                admittedAt = encounter.AdmittedAt,
                changedAt = DateTimeOffset.UtcNow
            }),
            PartitionKey = encounterId.ToString(),
            CreatedAt = DateTimeOffset.UtcNow
        });

        await _db.SaveChangesAsync();

        await _audit.WriteAsync(
            AuditAction.EncounterStatusChanged,
            "Encounter",
            encounterId,
            previousValue: new { status = previousStatus.ToDbString() },
            newValue: new { status = targetStatus.ToDbString(), dischargeDiagnosis });

        return new EncounterStatusTransitionResult(encounterId, targetStatus);
    }

    public async Task<object> GetTimelineAsync(Guid encounterId)
    {
        var encounter = await _db.Encounters.FindAsync(encounterId);
        if (encounter is null)
            throw new NotFoundException("Encounter not found.", "ENCOUNTER_NOT_FOUND");

        var events = new List<object>();

        events.Add(new
        {
            type = "status",
            timestamp = encounter.AdmittedAt,
            status = EncounterStatus.Active.ToDbString(),
            label = "Patient admitted"
        });

        if (encounter.DischargedAt is not null)
        {
            events.Add(new
            {
                type = "status",
                timestamp = encounter.DischargedAt.Value,
                status = EncounterStatus.Discharged.ToDbString(),
                label = "Patient discharged"
            });
        }

        var observations = await _db.Observations
            .Where(o => o.EncounterId == encounterId)
            .Select(o => new
            {
                type = "observation",
                timestamp = o.RecordedAt,
                o.ObservationCode,
                o.Value,
                o.Unit
            })
            .ToListAsync();
        events.AddRange(observations);

        var alerts = await _db.ClinicalAlerts
            .Where(a => a.EncounterId == encounterId)
            .Select(a => new
            {
                type = "alert",
                timestamp = a.TriggeredAt,
                alertType = a.AlertType.ToString(),
                severity = a.Severity.ToString(),
                status = a.Status.ToString()
            })
            .ToListAsync();
        events.AddRange(alerts);

        var orders = await _db.Orders
            .Where(o => o.EncounterId == encounterId)
            .Select(o => new
            {
                type = "order",
                timestamp = o.OrderedAt,
                o.Description,
                orderType = o.OrderType.ToString(),
                status = o.Status.ToString()
            })
            .ToListAsync();
        events.AddRange(orders);

        var medications = await _db.MedicationAdministrations
            .Where(m => m.EncounterId == encounterId)
            .Select(m => new
            {
                type = "medication",
                timestamp = m.AdministeredAt,
                m.DrugName,
                m.Dose,
                m.DoseUnit,
                m.Route
            })
            .ToListAsync();
        events.AddRange(medications);

        var timeline = events
            .OrderByDescending(e => (DateTimeOffset)((dynamic)e).timestamp)
            .ToList();

        return new { encounterId, events = timeline };
    }

    public async Task<Encounter> OpenOrUpdateByIdentifierAsync(FhirEncounterUpsertRequest req)
    {
        var existingId = await _identifiers.ResolveInternalIdAsync(
            ExternalResourceType.Encounter, req.IdentifierSystem, req.IdentifierValue);

        if (existingId.HasValue)
        {
            var existingEncounter = await _db.Encounters
                .Include(e => e.Patient)
                .FirstOrDefaultAsync(e => e.Id == existingId.Value)
                ?? throw new NotFoundException("Encounter not found.", "ENCOUNTER_NOT_FOUND");

            existingEncounter.Department = req.Department;
            existingEncounter.AttendingPhysician = req.AttendingPhysician;
            existingEncounter.RoomBed = req.RoomBed;
            existingEncounter.AdmissionReason = req.AdmissionReason;

            if (existingEncounter.Status != req.TargetStatus)
            {
                await TransitionStatusAsync(
                    existingEncounter.Id, req.TargetStatus, req.DischargeDiagnosis);
                await _db.Entry(existingEncounter).ReloadAsync();
            }

            return existingEncounter;
        }

        // Create new encounter — mirrors PatientService.OpenEncounterAsync
        var patient = await _db.Patients.FindAsync(req.PatientId);
        if (patient is null)
            throw new NotFoundException("Patient not found.", "PATIENT_NOT_FOUND");

        var encounter = new Encounter
        {
            Id = Guid.NewGuid(),
            PatientId = req.PatientId,
            EncounterType = req.EncounterType,
            Status = EncounterStatus.Active,
            Department = req.Department,
            AttendingPhysician = req.AttendingPhysician,
            RoomBed = req.RoomBed,
            AdmissionReason = req.AdmissionReason,
            AdmittedAt = req.AdmittedAt ?? DateTimeOffset.UtcNow,
            CreatedAt = DateTimeOffset.UtcNow
        };

        _db.Encounters.Add(encounter);

        _db.OutboxEvents.Add(new OutboxEvent
        {
            Id = Guid.NewGuid(),
            Topic = "encounter.status.changed",
            Payload = JsonSerializer.Serialize(new
            {
                encounterId = encounter.Id,
                patientId = patient.Id,
                mrn = patient.Mrn,
                patientName = $"{patient.FirstName} {patient.LastName}",
                previousStatus = (string?)null,
                newStatus = encounter.Status.ToDbString(),
                department = encounter.Department.ToDbString(),
                attendingPhysician = encounter.AttendingPhysician,
                roomBed = encounter.RoomBed,
                admissionReason = encounter.AdmissionReason,
                admittedAt = encounter.AdmittedAt,
                changedAt = DateTimeOffset.UtcNow
            }),
            PartitionKey = encounter.Id.ToString(),
            CreatedAt = DateTimeOffset.UtcNow
        });

        await _db.SaveChangesAsync();

        await _identifiers.LinkAsync(
            ExternalResourceType.Encounter,
            encounter.Id,
            req.IdentifierSystem,
            req.IdentifierValue);

        if (req.TargetStatus == EncounterStatus.Discharged)
            await TransitionStatusAsync(encounter.Id, EncounterStatus.Discharged, req.DischargeDiagnosis);

        return encounter;
    }
}
