using Hl7.Fhir.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using System.Globalization;
using FhirPatient = Hl7.Fhir.Model.Patient;
using FhirEncounter = Hl7.Fhir.Model.Encounter;
using FhirObservation = Hl7.Fhir.Model.Observation;

public class FhirService : IFhirService
{
    private readonly AppDbContext _db;
    private readonly FhirOptions _options;

    public FhirService(AppDbContext db, IOptions<FhirOptions> options)
    {
        _db = db;
        _options = options.Value;
    }

    // --- Patient ---

    public async Task<FhirPatient?> GetPatientAsync(Guid id)
    {
        var entity = await _db.Patients.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id);
        return entity is null ? null : PatientMapper.ToFhir(entity, _options.BaseUrl);
    }

    public async Task<Bundle> SearchPatientsAsync(
        string? name, string? birthdate, string? identifier, int count, int offset)
    {
        count = Math.Clamp(count, 1, 100);

        var query = _db.Patients.AsNoTracking().AsQueryable();

        if (!string.IsNullOrWhiteSpace(name))
            query = query.Where(p => EF.Functions.ILike(p.FullName, $"%{name}%"));

        if (!string.IsNullOrWhiteSpace(birthdate) && DateOnly.TryParse(birthdate, out var dob))
            query = query.Where(p => p.DateOfBirth == dob);

        if (!string.IsNullOrWhiteSpace(identifier))
            query = query.Where(p => p.Mrn == identifier);

        var total = await query.CountAsync();
        var entities = await query.OrderBy(p => p.FullName).Skip(offset).Take(count).ToListAsync();

        return BuildSearchBundle(
            entities.Select(e => PatientMapper.ToFhir(e, _options.BaseUrl)).Cast<Resource>().ToList(),
            total, count, offset, "Patient");
    }

    // --- Encounter ---

    public async Task<FhirEncounter?> GetEncounterAsync(Guid id)
    {
        var entity = await _db.Encounters.AsNoTracking().FirstOrDefaultAsync(e => e.Id == id);
        return entity is null ? null : EncounterMapper.ToFhir(entity, _options.BaseUrl);
    }

    public async Task<Bundle> SearchEncountersAsync(
        string? patient, string? status, string? date, int count, int offset)
    {
        count = Math.Clamp(count, 1, 100);

        var query = _db.Encounters.AsNoTracking().AsQueryable();

        if (!string.IsNullOrWhiteSpace(patient) && Guid.TryParse(patient, out var patientId))
            query = query.Where(e => e.PatientId == patientId);

        if (!string.IsNullOrWhiteSpace(status))
        {
            var fhirStatus = status.ToLowerInvariant();
            query = fhirStatus switch
            {
                "in-progress" => query.Where(e => e.Status == "active"),
                "finished" => query.Where(e => e.Status == "discharged"),
                _ => query.Where(e => e.Status == status),
            };
        }

        if (!string.IsNullOrWhiteSpace(date) && TryParseUtcDate(date, out var encounterDay))
        {
            var dayEnd = encounterDay.AddDays(1);
            query = query.Where(e =>
                e.AdmissionDate != null &&
                e.AdmissionDate.Value >= encounterDay &&
                e.AdmissionDate.Value < dayEnd);
        }

        var total = await query.CountAsync();
        var entities = await query.OrderByDescending(e => e.AdmissionDate).Skip(offset).Take(count).ToListAsync();

        return BuildSearchBundle(
            entities.Select(e => EncounterMapper.ToFhir(e, _options.BaseUrl)).Cast<Resource>().ToList(),
            total, count, offset, "Encounter");
    }

    // --- Observation ---

    public async Task<FhirObservation?> GetObservationAsync(Guid id)
    {
        var entity = await _db.Observations.AsNoTracking().FirstOrDefaultAsync(o => o.Id == id);
        return entity is null ? null : ObservationMapper.ToFhir(entity, _options.BaseUrl);
    }

    public async Task<Bundle> SearchObservationsAsync(
        string? patient, string? code, string? date,
        string? category, string? encounter, int count, int offset)
    {
        count = Math.Clamp(count, 1, 200);

        var query = _db.Observations.AsNoTracking().AsQueryable();

        if (!string.IsNullOrWhiteSpace(patient) && Guid.TryParse(patient, out var patientId))
            query = query.Where(o => o.PatientId == patientId);

        if (!string.IsNullOrWhiteSpace(encounter) && Guid.TryParse(encounter, out var encId))
            query = query.Where(o => o.EncounterId == encId);

        if (!string.IsNullOrWhiteSpace(code))
        {
            // Accept both LOINC codes (e.g., "8867-4") and VigilCare codes (e.g., "HEART_RATE")
            var loincToVigilCare = ObservationMapper.GetReverseLoincMapping();
            var vigilCareCode = loincToVigilCare.GetValueOrDefault(code, code);
            query = query.Where(o => o.ObservationCode == vigilCareCode);
        }

        if (!string.IsNullOrWhiteSpace(category))
        {
            var isVitalSigns = category.Equals("vital-signs", StringComparison.OrdinalIgnoreCase);
            var vitalCodes = ObservationMapper.GetVitalSignCodes();
            query = isVitalSigns
                ? query.Where(o => vitalCodes.Contains(o.ObservationCode))
                : query.Where(o => !vitalCodes.Contains(o.ObservationCode));
        }

        if (!string.IsNullOrWhiteSpace(date))
            query = ApplyObservationDateFilter(query, date);

        var total = await query.CountAsync();
        var entities = await query.OrderByDescending(o => o.RecordedAt).Skip(offset).Take(count).ToListAsync();

        return BuildSearchBundle(
            entities.Select(e => ObservationMapper.ToFhir(e, _options.BaseUrl)).Cast<Resource>().ToList(),
            total, count, offset, "Observation");
    }

    public async Task<Bundle?> GetPatientEverythingAsync(Guid patientId)
    {
        var patient = await _db.Patients.AsNoTracking().FirstOrDefaultAsync(p => p.Id == patientId);
        if (patient is null) return null;

        var encounters = await _db.Encounters.AsNoTracking()
            .Where(e => e.PatientId == patientId)
            .ToListAsync();

        var observations = await _db.Observations.AsNoTracking()
            .Where(o => o.PatientId == patientId)
            .OrderByDescending(o => o.RecordedAt)
            .ToListAsync();

        var bundle = new Bundle
        {
            Type = Bundle.BundleType.Searchset,
            Total = 1 + encounters.Count + observations.Count,
            Meta = new Meta { LastUpdated = DateTimeOffset.UtcNow },
        };

        bundle.Entry.Add(new Bundle.EntryComponent
        {
            FullUrl = $"{_options.BaseUrl}/Patient/{patient.Id}",
            Resource = PatientMapper.ToFhir(patient, _options.BaseUrl),
            Search = new Bundle.SearchComponent { Mode = Bundle.SearchEntryMode.Match },
        });

        foreach (var enc in encounters)
        {
            bundle.Entry.Add(new Bundle.EntryComponent
            {
                FullUrl = $"{_options.BaseUrl}/Encounter/{enc.Id}",
                Resource = EncounterMapper.ToFhir(enc, _options.BaseUrl),
                Search = new Bundle.SearchComponent { Mode = Bundle.SearchEntryMode.Include },
            });
        }

        foreach (var obs in observations)
        {
            bundle.Entry.Add(new Bundle.EntryComponent
            {
                FullUrl = $"{_options.BaseUrl}/Observation/{obs.Id}",
                Resource = ObservationMapper.ToFhir(obs, _options.BaseUrl),
                Search = new Bundle.SearchComponent { Mode = Bundle.SearchEntryMode.Include },
            });
        }

        return bundle;
    }

    // --- Bundle builder ---

    private Bundle BuildSearchBundle(
        List<Resource> resources, int total, int count, int offset, string resourceType)
    {
        var bundle = new Bundle
        {
            Type = Bundle.BundleType.Searchset,
            Total = total,
            Meta = new Meta { LastUpdated = DateTimeOffset.UtcNow },
        };

        foreach (var resource in resources)
        {
            bundle.Entry.Add(new Bundle.EntryComponent
            {
                FullUrl = $"{_options.BaseUrl}/{resourceType}/{resource.Id}",
                Resource = resource,
                Search = new Bundle.SearchComponent { Mode = Bundle.SearchEntryMode.Match },
            });
        }

        // Pagination links
        var selfUrl = $"{_options.BaseUrl}/{resourceType}?_count={count}&_offset={offset}";
        bundle.Link.Add(new Bundle.LinkComponent { Relation = "self", Url = selfUrl });

        if (offset + count < total)
        {
            var nextUrl = $"{_options.BaseUrl}/{resourceType}?_count={count}&_offset={offset + count}";
            bundle.Link.Add(new Bundle.LinkComponent { Relation = "next", Url = nextUrl });
        }

        if (offset > 0)
        {
            var prevOffset = Math.Max(0, offset - count);
            var prevUrl = $"{_options.BaseUrl}/{resourceType}?_count={count}&_offset={prevOffset}";
            bundle.Link.Add(new Bundle.LinkComponent { Relation = "previous", Url = prevUrl });
        }

        return bundle;
    }

    // --- CapabilityStatement ---

    public CapabilityStatement GetCapabilityStatement()
    {
        return new CapabilityStatement
        {
            Status = PublicationStatus.Active,
            Date = "2026-06-27",
            Kind = CapabilityStatementKind.Instance,
            FhirVersion = FHIRVersion.N4_0_1,
            Format = new[] { "json" },
            Software = new CapabilityStatement.SoftwareComponent
            {
                Name = _options.PublisherName,
                Version = _options.ServerVersion,
            },
            Implementation = new CapabilityStatement.ImplementationComponent
            {
                Description = "VigilCare Records FHIR R4 API — read-only access to promoted clinical data",
                Url = _options.BaseUrl,
            },
            Rest = new List<CapabilityStatement.RestComponent>
            {
                new()
                {
                    Mode = CapabilityStatement.RestfulCapabilityMode.Server,
                    Resource = new List<CapabilityStatement.ResourceComponent>
                    {
                        FhirResource("Patient", new[] { "read", "search-type" },
                            new[] { "name", "birthdate", "identifier" }),
                        FhirResource("Encounter", new[] { "read", "search-type" },
                            new[] { "patient", "status", "date" }),
                        FhirResource("Observation", new[] { "read", "search-type" },
                            new[] { "patient", "code", "date", "category", "encounter" }),
                    },
                }
            }
        };
    }

    private static CapabilityStatement.ResourceComponent FhirResource(
        string type, string[] interactions, string[] searchParams)
    {
        var resource = new CapabilityStatement.ResourceComponent
        {
            Type = type,
        };

        foreach (var interaction in interactions)
        {
            resource.Interaction.Add(new CapabilityStatement.ResourceInteractionComponent
            {
                Code = Enum.Parse<CapabilityStatement.TypeRestfulInteraction>(
                    interaction.Replace("-", ""), ignoreCase: true),
            });
        }

        foreach (var param in searchParams)
        {
            resource.SearchParam.Add(new CapabilityStatement.SearchParamComponent
            {
                Name = param,
                Type = SearchParamType.String,
            });
        }

        return resource;
    }

    private static IQueryable<Observation> ApplyObservationDateFilter(
        IQueryable<Observation> query, string date)
    {
        if (date.StartsWith("gt", StringComparison.OrdinalIgnoreCase)
            && TryParseUtcDate(date[2..], out var gt))
        {
            return query.Where(o => o.RecordedAt > gt);
        }

        if (date.StartsWith("lt", StringComparison.OrdinalIgnoreCase)
            && TryParseUtcDate(date[2..], out var lt))
        {
            return query.Where(o => o.RecordedAt < lt);
        }

        if (date.StartsWith("ge", StringComparison.OrdinalIgnoreCase)
            && TryParseUtcDate(date[2..], out var ge))
        {
            return query.Where(o => o.RecordedAt >= ge);
        }

        if (date.StartsWith("le", StringComparison.OrdinalIgnoreCase)
            && TryParseUtcDate(date[2..], out var le))
        {
            return query.Where(o => o.RecordedAt <= le);
        }

        if (TryParseUtcDate(date, out var dayStart))
        {
            var dayEnd = dayStart.AddDays(1);
            return query.Where(o => o.RecordedAt >= dayStart && o.RecordedAt < dayEnd);
        }

        return query;
    }

    private static bool TryParseUtcDate(string value, out DateTimeOffset utc)
    {
        if (DateTimeOffset.TryParse(
                value,
                CultureInfo.InvariantCulture,
                DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
                out utc))
        {
            return true;
        }

        utc = default;
        return false;
    }
}