using System.Globalization;
using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

public class OcrDraftPreFiller
{
    private static readonly Dictionary<string, string> DefaultUnits = new(StringComparer.Ordinal)
    {
        ["HEART_RATE"] = "bpm",
        ["TEMP_C"] = "C",
        ["BP_SYSTOLIC"] = "mmHg",
        ["BP_DIASTOLIC"] = "mmHg",
        ["RESP_RATE"] = "breaths/min",
        ["SPO2"] = "%",
        ["POTASSIUM_MEQ_L"] = "mEq/L",
        ["WBC_K_UL"] = "K/uL",
        ["GLUCOSE_MG_DL"] = "mg/dL",
        ["LACTATE_MMOL_L"] = "mmol/L",
    };

    private static readonly Regex ObservationValueField =
        new(@"^observation\.([^.]+)\.value$", RegexOptions.Compiled | RegexOptions.CultureInvariant);

    private readonly AppDbContext _db;
    private readonly OcrOptions _options;
    private readonly ILogger<OcrDraftPreFiller> _logger;

    public OcrDraftPreFiller(
        AppDbContext db,
        IOptions<OcrOptions> options,
        ILogger<OcrDraftPreFiller> logger)
    {
        _db = db;
        _options = options.Value;
        _logger = logger;
    }

    public async Task PreFillAsync(
        Guid batchId, BatchType batchType, OcrExtractionResult extraction)
    {
        var requirements = BatchTypeFieldRequirements.ForBatchType(batchType);
        var confidentFields = extraction.Fields
            .Where(f => f.Confidence >= _options.ConfidenceThreshold)
            .GroupBy(f => f.FieldName, StringComparer.Ordinal)
            .ToDictionary(g => g.Key, g => g.First(), StringComparer.Ordinal);

        if (requirements.ShowPatientDemographics)
            await PreFillPatientAsync(batchId, confidentFields);

        if (requirements.ShowEncounterContext)
            await PreFillEncounterAsync(batchId, confidentFields);

        if (requirements.ShowObservations)
            await PreFillObservationsAsync(batchId, confidentFields);

        await _db.SaveChangesAsync();
    }

    private async Task PreFillPatientAsync(
        Guid batchId, Dictionary<string, OcrExtractedField> fields)
    {
        if (!fields.Keys.Any(k => k.StartsWith("patient.", StringComparison.Ordinal)))
            return;

        var patient = await _db.DraftPatients.FirstOrDefaultAsync(p => p.BatchId == batchId);
        var now = DateTimeOffset.UtcNow;

        if (patient is null)
        {
            patient = new DraftPatient
            {
                Id = Guid.NewGuid(),
                BatchId = batchId,
                CreatedAt = now,
                UpdatedAt = now
            };
            _db.DraftPatients.Add(patient);
        }

        if (fields.TryGetValue("patient.fullName", out var name))
            patient.FullName = name.RawValue.Trim();

        if (fields.TryGetValue("patient.dateOfBirth", out var dob)
            && TryParseDate(dob.RawValue, out var parsedDob))
            patient.DateOfBirth = parsedDob;

        if (fields.TryGetValue("patient.sex", out var sex))
            patient.Sex = NormalizeSex(sex.RawValue);

        patient.UpdatedAt = now;
    }

    private async Task PreFillEncounterAsync(
        Guid batchId, Dictionary<string, OcrExtractedField> fields)
    {
        if (!fields.Keys.Any(k => k.StartsWith("encounter.", StringComparison.Ordinal)))
            return;

        var encounter = await _db.DraftEncounters.FirstOrDefaultAsync(e => e.BatchId == batchId);
        var now = DateTimeOffset.UtcNow;

        if (encounter is null)
        {
            encounter = new DraftEncounter
            {
                Id = Guid.NewGuid(),
                BatchId = batchId,
                CreatedAt = now,
                UpdatedAt = now
            };
            _db.DraftEncounters.Add(encounter);
        }

        if (fields.TryGetValue("encounter.admissionDate", out var admissionDate)
            && TryParseDateTime(admissionDate.RawValue, out var parsedAdmission))
            encounter.AdmissionDate = parsedAdmission;

        if (fields.TryGetValue("encounter.department", out var department)
            && TryParseDepartment(department.RawValue, out var parsedDepartment))
            encounter.Department = parsedDepartment;

        if (fields.TryGetValue("encounter.roomBed", out var roomBed))
            encounter.RoomBed = roomBed.RawValue.Trim();

        if (fields.TryGetValue("encounter.admissionReason", out var admissionReason))
            encounter.AdmissionReason = admissionReason.RawValue.Trim();

        encounter.UpdatedAt = now;
    }

    private async Task PreFillObservationsAsync(
        Guid batchId, Dictionary<string, OcrExtractedField> fields)
    {
        var observationCodes = fields.Keys
            .Select(k => ObservationValueField.Match(k))
            .Where(m => m.Success)
            .Select(m => m.Groups[1].Value)
            .Distinct(StringComparer.Ordinal)
            .ToList();

        if (observationCodes.Count == 0)
            return;

        var existingCodes = await _db.DraftObservations
            .Where(o => o.BatchId == batchId)
            .Select(o => o.ObservationCode)
            .ToListAsync();

        var existing = existingCodes.ToHashSet(StringComparer.Ordinal);
        var now = DateTimeOffset.UtcNow;

        foreach (var code in observationCodes)
        {
            if (existing.Contains(code))
                continue;

            if (!fields.TryGetValue($"observation.{code}.value", out var valueField))
                continue;

            if (!decimal.TryParse(
                    valueField.RawValue,
                    NumberStyles.Number,
                    CultureInfo.InvariantCulture,
                    out var value)
                && !decimal.TryParse(valueField.RawValue, out value))
            {
                _logger.LogDebug(
                    "Skipping OCR observation {Code} for batch {BatchId}: unparsable value '{Value}'",
                    code, batchId, valueField.RawValue);
                continue;
            }

            if (!PlausibilityValidator.IsPlausible(code, value, out var reason))
            {
                _logger.LogDebug(
                    "Skipping OCR observation {Code} for batch {BatchId}: {Reason}",
                    code, batchId, reason);
                continue;
            }

            var unit = fields.TryGetValue($"observation.{code}.unit", out var unitField)
                ? unitField.RawValue.Trim()
                : DefaultUnits.GetValueOrDefault(code, string.Empty);

            var recordedAt = now;
            if (fields.TryGetValue($"observation.{code}.recordedAt", out var recordedAtField)
                && TryParseDateTime(recordedAtField.RawValue, out var parsedRecordedAt))
                recordedAt = parsedRecordedAt;

            _db.DraftObservations.Add(new DraftObservation
            {
                Id = Guid.NewGuid(),
                BatchId = batchId,
                ObservationCode = code,
                Value = value,
                Unit = unit,
                RecordedAt = recordedAt,
                CreatedAt = now
            });
        }
    }

    private static bool TryParseDate(string raw, out DateOnly result)
    {
        if (DateOnly.TryParse(raw.Trim(), CultureInfo.InvariantCulture, DateTimeStyles.None, out result))
            return true;

        return DateOnly.TryParse(raw.Trim(), out result);
    }

    private static bool TryParseDateTime(string raw, out DateTimeOffset result)
    {
        if (DateTimeOffset.TryParse(raw.Trim(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result))
            return true;

        if (TryParseDate(raw, out var dateOnly))
        {
            result = new DateTimeOffset(dateOnly.ToDateTime(TimeOnly.MinValue), TimeSpan.Zero);
            return true;
        }

        return DateTimeOffset.TryParse(raw.Trim(), out result);
    }

    private static bool TryParseDepartment(string raw, out Department result)
    {
        var trimmed = raw.Trim();
        if (DepartmentExtensions.TryFromDbString(trimmed, out result))
            return true;

        foreach (Department department in Enum.GetValues<Department>())
        {
            if (department.ToDbString().Equals(trimmed, StringComparison.OrdinalIgnoreCase))
            {
                result = department;
                return true;
            }
        }

        result = default;
        return false;
    }

    private static string NormalizeSex(string raw) =>
        raw.Trim().ToLowerInvariant() switch
        {
            "m" or "male" => "Male",
            "f" or "female" => "Female",
            _ => raw.Trim()
        };
}
