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

public class PatientService : IPatientService
{
    private readonly AppDbContext _db;
    private readonly IExternalIdentifierService _identifiers;
    private readonly IAuditService _audit;
    private readonly PatientOptions _options;
    private readonly IPhiEncryptionService _crypto;
    private readonly IPhiAccessLogService _phiAccess;
    private readonly IHttpContextAccessor _http;

    public PatientService(
        AppDbContext db,
        IExternalIdentifierService identifiers,
        IAuditService audit,
        IOptions<PatientOptions> options,
        IPhiEncryptionService crypto,
        IPhiAccessLogService phiAccess,
        IHttpContextAccessor http)
    {
        _db = db;
        _identifiers = identifiers;
        _audit = audit;
        _options = options.Value;
        _crypto = crypto;
        _phiAccess = phiAccess;
        _http = http;
    }

    public async Task<Patient> RegisterAsync(RegisterPatientRequest req)
    {
        var patient = new Patient
        {
            Id = Guid.NewGuid(),
            Mrn = await GenerateMrnAsync(),
            FirstName = req.FirstName,
            LastName = req.LastName,
            DateOfBirth = req.DateOfBirth,
            Gender = req.Gender,
            BloodType = req.BloodType,
            Allergies = req.Allergies,
            EmergencyContactName = req.EmergencyContactName,
            EmergencyContactPhone = req.EmergencyContactPhone,
            CreatedAt = DateTimeOffset.UtcNow
        };
        SetNameSearchToken(patient);
        _db.Patients.Add(patient);

        try
        {
            await _db.SaveChangesAsync();
        }
        catch (DbUpdateException ex) when (IsMrnUniqueViolation(ex))
        {
            _db.Entry(patient).State = EntityState.Detached;
            patient.Mrn = await GenerateMrnAsync();
            _db.Patients.Add(patient);
            await _db.SaveChangesAsync();
        }

        await _audit.WriteAsync(
            AuditAction.PatientRegistered,
            "Patient",
            patient.Id,
            newValue: new { patient.Mrn });

        var path = _http.HttpContext?.Request.Path.Value ?? "/api/v1/patients";
        await _phiAccess.LogCreateAsync(patient.Id, path);

        return patient;
    }

    private static bool IsMrnUniqueViolation(DbUpdateException ex)
    {
        return ex.InnerException is Npgsql.PostgresException pg
            && pg.SqlState == "23505"
            && pg.ConstraintName?.Contains("mrn") == true;
    }

    public async Task<PagedResult<Patient>> ListAsync(
        string? q, int page, int pageSize)
    {
        var query = _db.Patients.AsQueryable();

        if (!string.IsNullOrWhiteSpace(q))
        {
            var parts = q.Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length == 2)
            {
                var token = _crypto.ComputeNameSearchToken(parts[0], parts[1]);
                query = query.Where(p => p.Mrn == q || p.NameSearchToken == token);
            }
            else
            {
                query = query.Where(p =>
                    p.Mrn == q ||
                    (p.NameSearchToken != null &&
                     (p.NameSearchToken == _crypto.ComputeNameSearchToken(q, "") ||
                      p.NameSearchToken == _crypto.ComputeNameSearchToken("", q))));
            }
        }

        var total = await query.CountAsync();
        var patients = await query
            .OrderBy(p => p.LastName).ThenBy(p => p.FirstName)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();

        var path = _http.HttpContext?.Request.Path.Value ?? "/api/v1/patients";
        await _phiAccess.LogListAsync(path, patients.Count, q);

        foreach (var p in patients)
            await _phiAccess.LogViewAsync(p.Id, $"{path}?page={page}");

        return new PagedResult<Patient>(patients, page, pageSize, total);
    }

    public async Task<Patient> UpdateAsync(Guid id, UpdatePatientRequest req)
    {
        var patient = await _db.Patients.FindAsync(id)
            ?? throw new NotFoundException("Patient not found.", "PATIENT_NOT_FOUND");

        var before = new
        {
            patient.FirstName, patient.LastName, patient.DateOfBirth,
            patient.Gender, patient.BloodType, patient.Allergies,
            patient.EmergencyContactName, patient.EmergencyContactPhone
        };

        if (req.FirstName is not null) patient.FirstName = req.FirstName;
        if (req.LastName is not null) patient.LastName = req.LastName;
        if (req.DateOfBirth is not null) patient.DateOfBirth = req.DateOfBirth.Value;
        if (req.Gender is not null) patient.Gender = req.Gender;
        if (req.BloodType is not null) patient.BloodType = req.BloodType;
        if (req.Allergies is not null) patient.Allergies = req.Allergies;
        if (req.EmergencyContactName is not null) patient.EmergencyContactName = req.EmergencyContactName;
        if (req.EmergencyContactPhone is not null) patient.EmergencyContactPhone = req.EmergencyContactPhone;

        if (req.FirstName is not null || req.LastName is not null)
            SetNameSearchToken(patient);

        await _db.SaveChangesAsync();

        await _audit.WriteAsync(
            AuditAction.PatientUpdated,
            "Patient",
            patient.Id,
            previousValue: before,
            newValue: new
            {
                patient.FirstName, patient.LastName, patient.DateOfBirth,
                patient.Gender, patient.BloodType, patient.Allergies,
                patient.EmergencyContactName, patient.EmergencyContactPhone
            });

        var path = _http.HttpContext?.Request.Path.Value ?? $"/api/v1/patients/{id}";
        await _phiAccess.LogUpdateAsync(patient.Id, path);

        return patient;
    }

    public async Task<Patient> GetByIdAsync(Guid id)
    {
        var patient = await _db.Patients
            .Include(p => p.Encounters.Where(e => e.Status == EncounterStatus.Active))
            .FirstOrDefaultAsync(p => p.Id == id);

        if (patient is null)
            throw new NotFoundException("Patient not found.", "PATIENT_NOT_FOUND");

        var path = _http.HttpContext?.Request.Path.Value ?? $"/api/v1/patients/{id}";
        await _phiAccess.LogViewAsync(patient.Id, path);

        return patient;
    }

    public async Task<Encounter> OpenEncounterAsync(Guid patientId, OpenEncounterRequest req)
    {
        var patient = await _db.Patients.FindAsync(patientId);
        if (patient is null)
            throw new NotFoundException("Patient not found.", "PATIENT_NOT_FOUND");

        var hasActiveOfType = await _db.Encounters.AnyAsync(e =>
            e.PatientId == patientId &&
            e.EncounterType == req.EncounterType &&
            e.Status == EncounterStatus.Active);

        if (hasActiveOfType)
            throw new ConflictException(
                "Patient already has an active encounter of this type.",
                "DUPLICATE_ACTIVE_ENCOUNTER");

        var encounter = new Encounter
        {
            Id = Guid.NewGuid(),
            PatientId = patientId,
            EncounterType = req.EncounterType,
            Status = EncounterStatus.Active,
            Department = req.Department,
            AttendingPhysician = req.AttendingPhysician,
            RoomBed = req.RoomBed,
            AdmissionReason = req.AdmissionReason,
            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
        });

        try
        {
            await _db.SaveChangesAsync();
        }
        catch (DbUpdateException ex)
            when (ex.InnerException is Npgsql.PostgresException { SqlState: "23505" } pg
                  && pg.ConstraintName == "ix_encounters_patient_active_type")
        {
            _db.ChangeTracker.Clear();
            throw new ConflictException(
                "Patient already has an active encounter of this type.",
                "DUPLICATE_ACTIVE_ENCOUNTER");
        }

        return encounter;
    }

    public async Task<Patient> RegisterOrUpdateByIdentifierAsync(FhirPatientUpsertRequest req)
    {
        var existingId = await _identifiers.ResolveInternalIdAsync(
            ExternalResourceType.Patient, req.IdentifierSystem, req.IdentifierValue);

        var fhirPath = _http.HttpContext?.Request.Path.Value ?? "/fhir/R4/Patient";

        if (existingId.HasValue)
        {
            var patient = await _db.Patients.FindAsync(existingId.Value)
                ?? throw new NotFoundException("Patient not found.", "PATIENT_NOT_FOUND");

            patient.FirstName = req.FirstName;
            patient.LastName = req.LastName;
            patient.DateOfBirth = req.DateOfBirth;
            patient.Gender = req.Gender;
            patient.BloodType = req.BloodType;
            patient.Allergies = req.Allergies;
            patient.EmergencyContactName = req.EmergencyContactName;
            patient.EmergencyContactPhone = req.EmergencyContactPhone;
            SetNameSearchToken(patient);

            await _db.SaveChangesAsync();
            await _phiAccess.LogUpdateAsync(patient.Id, fhirPath);
            return patient;
        }

        var mrn = req.IdentifierValue.Length <= 20
            ? req.IdentifierValue
            : await GenerateMrnAsync();

        var newPatient = new Patient
        {
            Id = Guid.NewGuid(),
            Mrn = mrn,
            FirstName = req.FirstName,
            LastName = req.LastName,
            DateOfBirth = req.DateOfBirth,
            Gender = req.Gender,
            BloodType = req.BloodType,
            Allergies = req.Allergies,
            EmergencyContactName = req.EmergencyContactName,
            EmergencyContactPhone = req.EmergencyContactPhone,
            CreatedAt = DateTimeOffset.UtcNow
        };
        SetNameSearchToken(newPatient);

        _db.Patients.Add(newPatient);
        await _db.SaveChangesAsync();

        await _identifiers.LinkAsync(
            ExternalResourceType.Patient,
            newPatient.Id,
            req.IdentifierSystem,
            req.IdentifierValue);

        await _phiAccess.LogCreateAsync(newPatient.Id, fhirPath);
        return newPatient;
    }

    private async Task<string> GenerateMrnAsync()
    {
        var seq = await _db.Database
            .SqlQueryRaw<long>("SELECT nextval('mrn_seq') AS \"Value\"")
            .SingleAsync();
        return $"{_options.MrnPrefix}-{seq.ToString($"D{_options.MrnDigits}")}";
    }

    private void SetNameSearchToken(Patient patient)
    {
        patient.NameSearchToken = _crypto.ComputeNameSearchToken(
            patient.FirstName, patient.LastName);
    }
}