using Media.JoshHeaps.Net.Models;

namespace Media.JoshHeaps.Net.Services;

public class MedicalDocsService(DbExecutor db, IWebHostEnvironment environment, EncryptionService encryption, ILogger<MedicalDocsService> logger)
{
    // --- People ---

    public async Task<List<MedicalPerson>> GetPeopleAsync(long userId)
    {
        try
        {
            return await db.ExecuteListReaderAsync(
                @"SELECT mp.id, mp.name, mp.date_of_birth, mp.notes, mp.created_at, mp.updated_at
                  FROM app.medical_people mp
                  JOIN app.medical_people_access mpa ON mpa.person_id = mp.id
                  WHERE mpa.user_id = @userId
                  ORDER BY mp.name",
                reader => new MedicalPerson
                {
                    Id = reader.GetInt64(0),
                    Name = reader.GetString(1),
                    DateOfBirth = reader.IsDBNull(2) ? null : reader.GetDateTime(2),
                    Notes = reader.IsDBNull(3) ? null : reader.GetString(3),
                    CreatedAt = reader.GetDateTime(4),
                    UpdatedAt = reader.GetDateTime(5)
                },
                new { userId });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get medical people");
            return [];
        }
    }

    public async Task<MedicalPerson?> CreatePersonAsync(long userId, string name, DateTime? dateOfBirth = null, string? notes = null)
    {
        try
        {
            var now = DateTime.UtcNow;
            var person = await db.ExecuteReaderAsync(
                @"INSERT INTO app.medical_people (name, date_of_birth, notes, created_at, updated_at)
                  VALUES (@name, @dateOfBirth, @notes, @createdAt, @updatedAt)
                  RETURNING id, name, date_of_birth, notes, created_at, updated_at",
                reader => new MedicalPerson
                {
                    Id = reader.GetInt64(0),
                    Name = reader.GetString(1),
                    DateOfBirth = reader.IsDBNull(2) ? null : reader.GetDateTime(2),
                    Notes = reader.IsDBNull(3) ? null : reader.GetString(3),
                    CreatedAt = reader.GetDateTime(4),
                    UpdatedAt = reader.GetDateTime(5)
                },
                new { name, dateOfBirth, notes, createdAt = now, updatedAt = now });

            if (person != null)
            {
                await db.ExecuteNonQueryAsync(
                    "INSERT INTO app.medical_people_access (person_id, user_id) VALUES (@personId, @userId) ON CONFLICT DO NOTHING",
                    new { personId = person.Id, userId });
            }

            return person;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to create medical person");
            return null;
        }
    }

    // --- People Access ---

    public async Task<bool> HasAccessToPersonAsync(long userId, long personId)
    {
        try
        {
            return await db.ExecuteAsync<bool>(
                "SELECT EXISTS(SELECT 1 FROM app.medical_people_access WHERE user_id = @userId AND person_id = @personId)",
                new { userId, personId });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to check person access");
            return false;
        }
    }

    public async Task<bool> GrantAccessAsync(long personId, long targetUserId)
    {
        try
        {
            await db.ExecuteNonQueryAsync(
                "INSERT INTO app.medical_people_access (person_id, user_id) VALUES (@personId, @userId) ON CONFLICT DO NOTHING",
                new { personId, userId = targetUserId });
            return true;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to grant person access");
            return false;
        }
    }

    public async Task<bool> RevokeAccessAsync(long personId, long targetUserId)
    {
        try
        {
            var count = await db.ExecuteAsync<long>(
                "SELECT COUNT(*) FROM app.medical_people_access WHERE person_id = @personId",
                new { personId });
            if (count <= 1)
                return false;

            await db.ExecuteNonQueryAsync(
                "DELETE FROM app.medical_people_access WHERE person_id = @personId AND user_id = @userId",
                new { personId, userId = targetUserId });
            return true;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to revoke person access");
            return false;
        }
    }

    public async Task<List<PersonAccessUser>> GetPeopleAccessAsync(long personId)
    {
        try
        {
            return await db.ExecuteListReaderAsync(
                @"SELECT u.id, u.username FROM app.users u
                  JOIN app.medical_people_access mpa ON mpa.user_id = u.id
                  WHERE mpa.person_id = @personId
                  ORDER BY u.username",
                reader => new PersonAccessUser
                {
                    Id = reader.GetInt64(0),
                    Username = reader.GetString(1)
                },
                new { personId });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get person access list");
            return [];
        }
    }

    public async Task<long?> GetPersonIdForResourceAsync(string resourceType, long resourceId)
    {
        try
        {
            var sql = resourceType switch
            {
                "document" => "SELECT person_id FROM app.medical_documents WHERE id = @id",
                "doctor" => "SELECT person_id FROM app.medical_doctors WHERE id = @id",
                "condition" => "SELECT person_id FROM app.medical_conditions WHERE id = @id",
                "prescription" => "SELECT person_id FROM app.medical_prescriptions WHERE id = @id",
                "pickup" => "SELECT p.person_id FROM app.medical_prescription_pickups pk JOIN app.medical_prescriptions p ON pk.prescription_id = p.id WHERE pk.id = @id",
                "provider" => "SELECT person_id FROM app.medical_billing_providers WHERE id = @id",
                "provider-payment" => "SELECT bp.person_id FROM app.medical_provider_payments pp JOIN app.medical_billing_providers bp ON pp.provider_id = bp.id WHERE pp.id = @id",
                "bill" => "SELECT person_id FROM app.medical_bills WHERE id = @id",
                "bill-charge" => "SELECT b.person_id FROM app.medical_bill_charges bc JOIN app.medical_bills b ON bc.bill_id = b.id WHERE bc.id = @id",
                _ => throw new ArgumentException($"Unknown resource type: {resourceType}")
            };
            return await db.ExecuteAsync<long?>(sql, new { id = resourceId });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get person ID for {ResourceType} {ResourceId}", resourceType, resourceId);
            return null;
        }
    }

    // --- Documents ---

    public async Task<MedicalDocument?> SaveDocumentAsync(long personId, IFormFile file, string? title = null, string? description = null, DateTime? documentDate = null, string? classification = null)
    {
        string? tempFilePath = null;
        string? encryptedFilePath = null;

        try
        {
            var fileExtension = Path.GetExtension(file.FileName);
            var uniqueFileName = $"{Guid.NewGuid()}{fileExtension}.enc";

            var mediaFolder = Path.Combine(environment.ContentRootPath, "App_Data", "medical", personId.ToString());
            Directory.CreateDirectory(mediaFolder);

            tempFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
            encryptedFilePath = Path.Combine(mediaFolder, uniqueFileName);

            using (var fileStream = new FileStream(tempFilePath, FileMode.Create))
            {
                await file.CopyToAsync(fileStream);
            }

            await encryption.EncryptFileAsync(tempFilePath, encryptedFilePath);

            File.Delete(tempFilePath);
            tempFilePath = null;

            var relativeFilePath = Path.Combine("App_Data", "medical", personId.ToString(), uniqueFileName);
            var now = DateTime.UtcNow;

            var doc = await db.ExecuteReaderAsync(
                @"INSERT INTO app.medical_documents (person_id, document_type, file_name, file_path, file_size, mime_type, is_encrypted, title, description, document_date, classification, created_at, updated_at)
                  VALUES (@personId, 'file', @fileName, @filePath, @fileSize, @mimeType, true, @title, @description, @documentDate, @classification, @createdAt, @updatedAt)
                  RETURNING id, person_id, document_type, file_name, file_path, file_size, mime_type, is_encrypted, title, description, document_date, classification, extracted_text, ai_processed, ai_processed_at, doctor_id, created_at, updated_at",
                MapDocument,
                new
                {
                    personId,
                    fileName = file.FileName,
                    filePath = relativeFilePath,
                    fileSize = file.Length,
                    mimeType = file.ContentType,
                    title,
                    description,
                    documentDate,
                    classification,
                    createdAt = now,
                    updatedAt = now
                });

            return doc;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to save medical document for person {PersonId}", personId);

            if (tempFilePath != null && File.Exists(tempFilePath))
            {
                try { File.Delete(tempFilePath); } catch { }
            }
            if (encryptedFilePath != null && File.Exists(encryptedFilePath))
            {
                try { File.Delete(encryptedFilePath); } catch { }
            }

            return null;
        }
    }

    public async Task<MedicalDocument?> SaveNoteAsync(long personId, string title, string description, DateTime? documentDate = null, string? classification = null)
    {
        try
        {
            var now = DateTime.UtcNow;

            return await db.ExecuteReaderAsync(
                @"INSERT INTO app.medical_documents (person_id, document_type, title, description, document_date, classification, is_encrypted, created_at, updated_at)
                  VALUES (@personId, 'note', @title, @description, @documentDate, @classification, false, @createdAt, @updatedAt)
                  RETURNING id, person_id, document_type, file_name, file_path, file_size, mime_type, is_encrypted, title, description, document_date, classification, extracted_text, ai_processed, ai_processed_at, doctor_id, created_at, updated_at",
                MapDocument,
                new { personId, title, description, documentDate, classification, createdAt = now, updatedAt = now });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to save medical note for person {PersonId}", personId);
            return null;
        }
    }

    public async Task<List<MedicalDocument>> GetDocumentsAsync(long? personId = null, int offset = 0, int limit = 50)
    {
        try
        {
            string query;
            object parameters;

            if (personId.HasValue)
            {
                query = @"SELECT id, person_id, document_type, file_name, file_path, file_size, mime_type, is_encrypted, title, description, document_date, classification, extracted_text, ai_processed, ai_processed_at, doctor_id, created_at, updated_at
                          FROM app.medical_documents
                          WHERE person_id = @personId
                          ORDER BY created_at DESC
                          OFFSET @offset LIMIT @limit";
                parameters = new { personId = personId.Value, offset, limit };
            }
            else
            {
                query = @"SELECT id, person_id, document_type, file_name, file_path, file_size, mime_type, is_encrypted, title, description, document_date, classification, extracted_text, ai_processed, ai_processed_at, doctor_id, created_at, updated_at
                          FROM app.medical_documents
                          ORDER BY created_at DESC
                          OFFSET @offset LIMIT @limit";
                parameters = new { offset, limit };
            }

            return await db.ExecuteListReaderAsync(query, MapDocument, parameters);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get medical documents");
            return [];
        }
    }

    public async Task<List<MedicalDocument>> SearchDocumentsAsync(long? personId = null, string? search = null, string? classification = null, string? documentType = null, long? doctorId = null, long? tagId = null, long? conditionId = null, DateTime? fromDate = null, DateTime? toDate = null, bool? aiProcessed = null, long? accessUserId = null, int offset = 0, int limit = 50)
    {
        try
        {
            var personFilter = personId.HasValue
                ? "person_id = @personId"
                : "person_id IN (SELECT person_id FROM app.medical_people_access WHERE user_id = @accessUserId)";
            var conditions = new List<string> { personFilter };

            if (!string.IsNullOrWhiteSpace(search))
                conditions.Add("(title ILIKE @search OR description ILIKE @search OR extracted_text ILIKE @search)");

            if (!string.IsNullOrEmpty(classification))
                conditions.Add("classification = @classification");

            if (!string.IsNullOrEmpty(documentType))
                conditions.Add("document_type = @documentType");

            if (doctorId.HasValue)
                conditions.Add("doctor_id = @doctorId");

            if (tagId.HasValue)
                conditions.Add("id IN (SELECT document_id FROM app.medical_document_tags WHERE tag_id = @tagId)");

            if (conditionId.HasValue)
                conditions.Add("id IN (SELECT document_id FROM app.medical_document_conditions WHERE condition_id = @conditionId)");

            if (fromDate.HasValue)
                conditions.Add("document_date >= @fromDate");

            if (toDate.HasValue)
                conditions.Add("document_date <= @toDate");

            if (aiProcessed.HasValue)
                conditions.Add("ai_processed = @aiProcessed");

            var whereClause = string.Join(" AND ", conditions);
            var query = $@"SELECT id, person_id, document_type, file_name, file_path, file_size, mime_type, is_encrypted, title, description, document_date, classification, extracted_text, ai_processed, ai_processed_at, doctor_id, created_at, updated_at
                          FROM app.medical_documents
                          WHERE {whereClause}
                          ORDER BY document_date DESC NULLS LAST, created_at DESC
                          OFFSET @offset LIMIT @limit";

            return await db.ExecuteListReaderAsync(query, MapDocument, new
            {
                personId = personId ?? 0L,
                accessUserId = accessUserId ?? 0L,
                search = !string.IsNullOrWhiteSpace(search) ? $"%{search}%" : (string?)null,
                classification,
                documentType,
                doctorId = doctorId ?? 0L,
                tagId = tagId ?? 0L,
                conditionId = conditionId ?? 0L,
                fromDate,
                toDate,
                aiProcessed = aiProcessed ?? false,
                offset,
                limit
            });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to search medical documents for person {PersonId}", personId);
            return [];
        }
    }

    public async Task<List<MedicalTag>> GetPersonTagsAsync(long? personId = null, long? accessUserId = null)
    {
        try
        {
            var personFilter = personId.HasValue
                ? "d.person_id = @personId"
                : "d.person_id IN (SELECT person_id FROM app.medical_people_access WHERE user_id = @accessUserId)";

            return await db.ExecuteListReaderAsync(
                $@"SELECT DISTINCT t.id, t.name, t.created_at
                  FROM app.medical_tags t
                  JOIN app.medical_document_tags dt ON dt.tag_id = t.id
                  JOIN app.medical_documents d ON dt.document_id = d.id
                  WHERE {personFilter}
                  ORDER BY t.name",
                reader => new MedicalTag
                {
                    Id = reader.GetInt64(0),
                    Name = reader.GetString(1),
                    CreatedAt = reader.GetDateTime(2)
                },
                new { personId = personId ?? 0L, accessUserId = accessUserId ?? 0L });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get tags for person {PersonId}", personId);
            return [];
        }
    }

    public async Task<MedicalDocument?> GetDocumentByIdAsync(long documentId)
    {
        try
        {
            return await db.ExecuteReaderAsync(
                @"SELECT id, person_id, document_type, file_name, file_path, file_size, mime_type, is_encrypted, title, description, document_date, classification, extracted_text, ai_processed, ai_processed_at, doctor_id, created_at, updated_at
                  FROM app.medical_documents
                  WHERE id = @documentId",
                MapDocument,
                new { documentId });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get medical document {DocumentId}", documentId);
            return null;
        }
    }

    public async Task<byte[]?> GetDecryptedDocumentDataAsync(long documentId)
    {
        try
        {
            var doc = await GetDocumentByIdAsync(documentId);
            if (doc == null || doc.DocumentType != "file" || doc.FilePath == null)
                return null;

            var fullPath = Path.Combine(environment.ContentRootPath, doc.FilePath);

            if (!File.Exists(fullPath))
            {
                logger.LogError("Medical document file not found at {FilePath}", fullPath);
                return null;
            }

            if (doc.IsEncrypted)
            {
                return await encryption.DecryptFileAsync(fullPath);
            }
            else
            {
                return await File.ReadAllBytesAsync(fullPath);
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get decrypted medical document data for {DocumentId}", documentId);
            return null;
        }
    }

    public async Task<bool> DeleteDocumentAsync(long documentId)
    {
        try
        {
            var doc = await GetDocumentByIdAsync(documentId);
            if (doc == null) return false;

            await db.ExecuteNonQueryAsync("DELETE FROM app.medical_documents WHERE id = @documentId", new { documentId });

            if (doc.DocumentType == "file" && doc.FilePath != null)
            {
                var physicalPath = Path.Combine(environment.ContentRootPath, doc.FilePath);
                if (File.Exists(physicalPath))
                {
                    File.Delete(physicalPath);
                }
            }

            return true;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to delete medical document {DocumentId}", documentId);
            return false;
        }
    }

    public async Task<bool> UpdateDocumentAsync(long docId, string? title, string? description, DateTime? documentDate, string? classification, long? doctorId)
    {
        try
        {
            var rows = await db.ExecuteNonQueryAsync(
                @"UPDATE app.medical_documents
                  SET title = @title,
                      description = @description,
                      document_date = @documentDate,
                      classification = @classification,
                      doctor_id = @doctorId,
                      updated_at = @updatedAt
                  WHERE id = @docId",
                new
                {
                    docId,
                    title,
                    description,
                    documentDate,
                    classification,
                    doctorId,
                    updatedAt = DateTime.UtcNow
                });
            return rows > 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to update document {DocumentId}", docId);
            return false;
        }
    }

    // --- Doctors ---

    public async Task<List<MedicalDoctor>> GetDoctorsAsync(long? personId = null, long? accessUserId = null)
    {
        try
        {
            var personFilter = personId.HasValue
                ? "person_id = @personId"
                : "person_id IN (SELECT person_id FROM app.medical_people_access WHERE user_id = @accessUserId)";

            return await db.ExecuteListReaderAsync(
                $"SELECT id, person_id, name, specialty, phone, address, notes, created_at, updated_at FROM app.medical_doctors WHERE {personFilter} ORDER BY name",
                reader => new MedicalDoctor
                {
                    Id = reader.GetInt64(0),
                    PersonId = reader.GetInt64(1),
                    Name = reader.GetString(2),
                    Specialty = reader.IsDBNull(3) ? null : reader.GetString(3),
                    Phone = reader.IsDBNull(4) ? null : reader.GetString(4),
                    Address = reader.IsDBNull(5) ? null : reader.GetString(5),
                    Notes = reader.IsDBNull(6) ? null : reader.GetString(6),
                    CreatedAt = reader.GetDateTime(7),
                    UpdatedAt = reader.GetDateTime(8)
                },
                new { personId = personId ?? 0L, accessUserId = accessUserId ?? 0L });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get doctors for person {PersonId}", personId);
            return [];
        }
    }

    public async Task<MedicalDoctor?> CreateDoctorAsync(long personId, string name, string? specialty = null, string? phone = null, string? address = null, string? notes = null)
    {
        try
        {
            var now = DateTime.UtcNow;
            return await db.ExecuteReaderAsync(
                @"INSERT INTO app.medical_doctors (person_id, name, specialty, phone, address, notes, created_at, updated_at)
                  VALUES (@personId, @name, @specialty, @phone, @address, @notes, @createdAt, @updatedAt)
                  RETURNING id, name, specialty, phone, address, notes, created_at, updated_at",
                reader => new MedicalDoctor
                {
                    Id = reader.GetInt64(0),
                    Name = reader.GetString(1),
                    Specialty = reader.IsDBNull(2) ? null : reader.GetString(2),
                    Phone = reader.IsDBNull(3) ? null : reader.GetString(3),
                    Address = reader.IsDBNull(4) ? null : reader.GetString(4),
                    Notes = reader.IsDBNull(5) ? null : reader.GetString(5),
                    CreatedAt = reader.GetDateTime(6),
                    UpdatedAt = reader.GetDateTime(7)
                },
                new { personId, name, specialty, phone, address, notes, createdAt = now, updatedAt = now });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to create doctor for person {PersonId}", personId);
            return null;
        }
    }

    public async Task<bool> UpdateDoctorAsync(long id, string name, string? specialty = null, string? phone = null, string? address = null, string? notes = null)
    {
        try
        {
            var rows = await db.ExecuteNonQueryAsync(
                @"UPDATE app.medical_doctors SET name = @name, specialty = @specialty, phone = @phone, address = @address, notes = @notes, updated_at = @updatedAt
                  WHERE id = @id",
                new { id, name, specialty, phone, address, notes, updatedAt = DateTime.UtcNow });
            return rows > 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to update doctor {DoctorId}", id);
            return false;
        }
    }

    public async Task<bool> DeleteDoctorAsync(long id)
    {
        try
        {
            var rows = await db.ExecuteNonQueryAsync("DELETE FROM app.medical_doctors WHERE id = @id", new { id });
            return rows > 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to delete doctor {DoctorId}", id);
            return false;
        }
    }

    public async Task<MedicalDoctor?> GetDoctorByIdAsync(long id)
    {
        try
        {
            return await db.ExecuteReaderAsync(
                "SELECT id, name, specialty, phone, address, notes, created_at, updated_at FROM app.medical_doctors WHERE id = @id",
                reader => new MedicalDoctor
                {
                    Id = reader.GetInt64(0),
                    Name = reader.GetString(1),
                    Specialty = reader.IsDBNull(2) ? null : reader.GetString(2),
                    Phone = reader.IsDBNull(3) ? null : reader.GetString(3),
                    Address = reader.IsDBNull(4) ? null : reader.GetString(4),
                    Notes = reader.IsDBNull(5) ? null : reader.GetString(5),
                    CreatedAt = reader.GetDateTime(6),
                    UpdatedAt = reader.GetDateTime(7)
                },
                new { id });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get doctor {DoctorId}", id);
            return null;
        }
    }

    // --- Conditions ---

    public async Task<List<MedicalCondition>> GetConditionsAsync(long? personId = null, long? accessUserId = null)
    {
        try
        {
            var personFilter = personId.HasValue
                ? "person_id = @personId"
                : "person_id IN (SELECT person_id FROM app.medical_people_access WHERE user_id = @accessUserId)";

            return await db.ExecuteListReaderAsync(
                $"SELECT id, person_id, name, diagnosed_date, notes, is_active, created_at, updated_at FROM app.medical_conditions WHERE {personFilter} ORDER BY name",
                reader => new MedicalCondition
                {
                    Id = reader.GetInt64(0),
                    PersonId = reader.GetInt64(1),
                    Name = reader.GetString(2),
                    DiagnosedDate = reader.IsDBNull(3) ? null : reader.GetDateTime(3),
                    Notes = reader.IsDBNull(4) ? null : reader.GetString(4),
                    IsActive = reader.GetBoolean(5),
                    CreatedAt = reader.GetDateTime(6),
                    UpdatedAt = reader.GetDateTime(7)
                },
                new { personId = personId ?? 0L, accessUserId = accessUserId ?? 0L });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get conditions for person {PersonId}", personId);
            return [];
        }
    }

    public async Task<MedicalCondition?> CreateConditionAsync(long personId, string name, DateTime? diagnosedDate = null, string? notes = null)
    {
        try
        {
            var now = DateTime.UtcNow;
            return await db.ExecuteReaderAsync(
                @"INSERT INTO app.medical_conditions (person_id, name, diagnosed_date, notes, created_at, updated_at)
                  VALUES (@personId, @name, @diagnosedDate, @notes, @createdAt, @updatedAt)
                  RETURNING id, person_id, name, diagnosed_date, notes, is_active, created_at, updated_at",
                reader => new MedicalCondition
                {
                    Id = reader.GetInt64(0),
                    PersonId = reader.GetInt64(1),
                    Name = reader.GetString(2),
                    DiagnosedDate = reader.IsDBNull(3) ? null : reader.GetDateTime(3),
                    Notes = reader.IsDBNull(4) ? null : reader.GetString(4),
                    IsActive = reader.GetBoolean(5),
                    CreatedAt = reader.GetDateTime(6),
                    UpdatedAt = reader.GetDateTime(7)
                },
                new { personId, name, diagnosedDate, notes, createdAt = now, updatedAt = now });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to create condition for person {PersonId}", personId);
            return null;
        }
    }

    public async Task<bool> UpdateConditionAsync(long id, string name, DateTime? diagnosedDate = null, string? notes = null, bool isActive = true)
    {
        try
        {
            var rows = await db.ExecuteNonQueryAsync(
                @"UPDATE app.medical_conditions SET name = @name, diagnosed_date = @diagnosedDate, notes = @notes, is_active = @isActive, updated_at = @updatedAt
                  WHERE id = @id",
                new { id, name, diagnosedDate, notes, isActive, updatedAt = DateTime.UtcNow });
            return rows > 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to update condition {ConditionId}", id);
            return false;
        }
    }

    public async Task<bool> DeleteConditionAsync(long id)
    {
        try
        {
            var rows = await db.ExecuteNonQueryAsync("DELETE FROM app.medical_conditions WHERE id = @id", new { id });
            return rows > 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to delete condition {ConditionId}", id);
            return false;
        }
    }

    // --- Prescriptions ---

    public async Task<List<MedicalPrescription>> GetPrescriptionsAsync(long? personId = null, long? accessUserId = null)
    {
        try
        {
            var personFilter = personId.HasValue
                ? "p.person_id = @personId"
                : "p.person_id IN (SELECT person_id FROM app.medical_people_access WHERE user_id = @accessUserId)";

            return await db.ExecuteListReaderAsync(
                $@"SELECT p.id, p.person_id, p.doctor_id, p.medication_name, p.dosage, p.frequency, p.is_active, p.start_date, p.end_date, p.notes, p.created_at, p.updated_at, p.rx_number,
                         d.name AS doctor_name,
                         (SELECT MAX(pk.pickup_date) FROM app.medical_prescription_pickups pk WHERE pk.prescription_id = p.id) AS last_pickup
                  FROM app.medical_prescriptions p
                  LEFT JOIN app.medical_doctors d ON p.doctor_id = d.id
                  WHERE {personFilter}
                  ORDER BY p.medication_name",
                reader => new MedicalPrescription
                {
                    Id = reader.GetInt64(0),
                    PersonId = reader.GetInt64(1),
                    DoctorId = reader.IsDBNull(2) ? null : reader.GetInt64(2),
                    MedicationName = reader.GetString(3),
                    Dosage = reader.IsDBNull(4) ? null : reader.GetString(4),
                    Frequency = reader.IsDBNull(5) ? null : reader.GetString(5),
                    IsActive = reader.GetBoolean(6),
                    StartDate = reader.IsDBNull(7) ? null : reader.GetDateTime(7),
                    EndDate = reader.IsDBNull(8) ? null : reader.GetDateTime(8),
                    Notes = reader.IsDBNull(9) ? null : reader.GetString(9),
                    CreatedAt = reader.GetDateTime(10),
                    UpdatedAt = reader.GetDateTime(11),
                    RxNumber = reader.IsDBNull(12) ? null : reader.GetString(12),
                    DoctorName = reader.IsDBNull(13) ? null : reader.GetString(13),
                    LastPickupDate = reader.IsDBNull(14) ? null : reader.GetDateTime(14)
                },
                new { personId = personId ?? 0L, accessUserId = accessUserId ?? 0L });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get prescriptions for person {PersonId}", personId);
            return [];
        }
    }

    public async Task<MedicalPrescription?> CreatePrescriptionAsync(long personId, string medicationName, string? dosage = null, string? frequency = null, long? doctorId = null, DateTime? startDate = null, string? notes = null, string? rxNumber = null)
    {
        try
        {
            var now = DateTime.UtcNow;
            return await db.ExecuteReaderAsync(
                @"INSERT INTO app.medical_prescriptions (person_id, doctor_id, medication_name, dosage, frequency, start_date, notes, rx_number, created_at, updated_at)
                  VALUES (@personId, @doctorId, @medicationName, @dosage, @frequency, @startDate, @notes, @rxNumber, @createdAt, @updatedAt)
                  RETURNING id, person_id, doctor_id, medication_name, dosage, frequency, is_active, start_date, end_date, notes, rx_number, created_at, updated_at",
                reader => new MedicalPrescription
                {
                    Id = reader.GetInt64(0),
                    PersonId = reader.GetInt64(1),
                    DoctorId = reader.IsDBNull(2) ? null : reader.GetInt64(2),
                    MedicationName = reader.GetString(3),
                    Dosage = reader.IsDBNull(4) ? null : reader.GetString(4),
                    Frequency = reader.IsDBNull(5) ? null : reader.GetString(5),
                    IsActive = reader.GetBoolean(6),
                    StartDate = reader.IsDBNull(7) ? null : reader.GetDateTime(7),
                    EndDate = reader.IsDBNull(8) ? null : reader.GetDateTime(8),
                    Notes = reader.IsDBNull(9) ? null : reader.GetString(9),
                    RxNumber = reader.IsDBNull(10) ? null : reader.GetString(10),
                    CreatedAt = reader.GetDateTime(11),
                    UpdatedAt = reader.GetDateTime(12)
                },
                new { personId, doctorId, medicationName, dosage, frequency, startDate, notes, rxNumber, createdAt = now, updatedAt = now });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to create prescription for person {PersonId}", personId);
            return null;
        }
    }

    public async Task<bool> UpdatePrescriptionAsync(long id, string medicationName, string? dosage = null, string? frequency = null, long? doctorId = null, DateTime? startDate = null, DateTime? endDate = null, string? notes = null, bool isActive = true, string? rxNumber = null)
    {
        try
        {
            var rows = await db.ExecuteNonQueryAsync(
                @"UPDATE app.medical_prescriptions SET medication_name = @medicationName, dosage = @dosage, frequency = @frequency, doctor_id = @doctorId, start_date = @startDate, end_date = @endDate, notes = @notes, is_active = @isActive, rx_number = @rxNumber, updated_at = @updatedAt
                  WHERE id = @id",
                new { id, medicationName, dosage, frequency, doctorId, startDate, endDate, notes, isActive, rxNumber, updatedAt = DateTime.UtcNow });
            return rows > 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to update prescription {PrescriptionId}", id);
            return false;
        }
    }

    public async Task<bool> DeletePrescriptionAsync(long id)
    {
        try
        {
            var rows = await db.ExecuteNonQueryAsync("DELETE FROM app.medical_prescriptions WHERE id = @id", new { id });
            return rows > 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to delete prescription {PrescriptionId}", id);
            return false;
        }
    }

    // --- Prescription Pickups ---

    public async Task<List<MedicalPrescriptionPickup>> GetPickupsAsync(long prescriptionId)
    {
        try
        {
            return await db.ExecuteListReaderAsync(
                "SELECT id, prescription_id, document_id, pickup_date, quantity, pharmacy, cost, notes, created_at FROM app.medical_prescription_pickups WHERE prescription_id = @prescriptionId ORDER BY pickup_date DESC",
                reader => new MedicalPrescriptionPickup
                {
                    Id = reader.GetInt64(0),
                    PrescriptionId = reader.GetInt64(1),
                    DocumentId = reader.IsDBNull(2) ? null : reader.GetInt64(2),
                    PickupDate = reader.GetDateTime(3),
                    Quantity = reader.IsDBNull(4) ? null : reader.GetString(4),
                    Pharmacy = reader.IsDBNull(5) ? null : reader.GetString(5),
                    Cost = reader.IsDBNull(6) ? null : reader.GetDecimal(6),
                    Notes = reader.IsDBNull(7) ? null : reader.GetString(7),
                    CreatedAt = reader.GetDateTime(8)
                },
                new { prescriptionId });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get pickups for prescription {PrescriptionId}", prescriptionId);
            return [];
        }
    }

    public async Task<MedicalPrescriptionPickup?> CreatePickupAsync(long prescriptionId, DateTime pickupDate, string? quantity = null, string? pharmacy = null, decimal? cost = null, string? notes = null, long? documentId = null)
    {
        try
        {
            return await db.ExecuteReaderAsync(
                @"INSERT INTO app.medical_prescription_pickups (prescription_id, pickup_date, quantity, pharmacy, cost, notes, document_id, created_at)
                  VALUES (@prescriptionId, @pickupDate, @quantity, @pharmacy, @cost, @notes, @documentId, @createdAt)
                  RETURNING id, prescription_id, document_id, pickup_date, quantity, pharmacy, cost, notes, created_at",
                reader => new MedicalPrescriptionPickup
                {
                    Id = reader.GetInt64(0),
                    PrescriptionId = reader.GetInt64(1),
                    DocumentId = reader.IsDBNull(2) ? null : reader.GetInt64(2),
                    PickupDate = reader.GetDateTime(3),
                    Quantity = reader.IsDBNull(4) ? null : reader.GetString(4),
                    Pharmacy = reader.IsDBNull(5) ? null : reader.GetString(5),
                    Cost = reader.IsDBNull(6) ? null : reader.GetDecimal(6),
                    Notes = reader.IsDBNull(7) ? null : reader.GetString(7),
                    CreatedAt = reader.GetDateTime(8)
                },
                new { prescriptionId, pickupDate, quantity, pharmacy, cost, notes, documentId, createdAt = DateTime.UtcNow });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to create pickup for prescription {PrescriptionId}", prescriptionId);
            return null;
        }
    }

    public async Task<bool> DeletePickupAsync(long id)
    {
        try
        {
            var rows = await db.ExecuteNonQueryAsync("DELETE FROM app.medical_prescription_pickups WHERE id = @id", new { id });
            return rows > 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to delete pickup {PickupId}", id);
            return false;
        }
    }

    // --- AI Prescription Helpers ---

    public async Task<MedicalPrescription?> FindActivePrescriptionByMedicationAsync(long personId, string medicationName)
    {
        try
        {
            return await db.ExecuteReaderAsync(
                @"SELECT id, person_id, doctor_id, medication_name, dosage, frequency, is_active, start_date, end_date, notes, rx_number, created_at, updated_at
                  FROM app.medical_prescriptions
                  WHERE person_id = @personId AND LOWER(medication_name) = LOWER(@medicationName) AND is_active = true
                  LIMIT 1",
                reader => new MedicalPrescription
                {
                    Id = reader.GetInt64(0),
                    PersonId = reader.GetInt64(1),
                    DoctorId = reader.IsDBNull(2) ? null : reader.GetInt64(2),
                    MedicationName = reader.GetString(3),
                    Dosage = reader.IsDBNull(4) ? null : reader.GetString(4),
                    Frequency = reader.IsDBNull(5) ? null : reader.GetString(5),
                    IsActive = reader.GetBoolean(6),
                    StartDate = reader.IsDBNull(7) ? null : reader.GetDateTime(7),
                    EndDate = reader.IsDBNull(8) ? null : reader.GetDateTime(8),
                    Notes = reader.IsDBNull(9) ? null : reader.GetString(9),
                    RxNumber = reader.IsDBNull(10) ? null : reader.GetString(10),
                    CreatedAt = reader.GetDateTime(11),
                    UpdatedAt = reader.GetDateTime(12)
                },
                new { personId, medicationName });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to find active prescription by medication for person {PersonId}", personId);
            return null;
        }
    }

    public async Task<bool> HasPickupOnDateAsync(long prescriptionId, DateTime date)
    {
        try
        {
            return await db.ExecuteReaderAsync(
                "SELECT EXISTS (SELECT 1 FROM app.medical_prescription_pickups WHERE prescription_id = @prescriptionId AND pickup_date::date = @date::date)",
                reader => reader.GetBoolean(0),
                new { prescriptionId, date });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to check pickup on date for prescription {PrescriptionId}", prescriptionId);
            return false;
        }
    }

    public async Task<MedicalBill?> FindSameDayPharmacyBillAsync(long providerId, DateTime billDate)
    {
        try
        {
            return await db.ExecuteReaderAsync(
                @"SELECT b.id, b.person_id, b.total_amount, b.summary, b.category, b.bill_date, b.doctor_id, b.provider_id, b.source, b.created_at, b.updated_at,
                         d.name AS doctor_name,
                         bp.name AS provider_name,
                         (SELECT string_agg(COALESCE(md.title, md.file_name, 'Document #' || md.id::text), ', ')
                          FROM app.medical_bill_documents bd
                          JOIN app.medical_documents md ON bd.document_id = md.id
                          WHERE bd.bill_id = b.id) AS document_names
                  FROM app.medical_bills b
                  LEFT JOIN app.medical_doctors d ON b.doctor_id = d.id
                  LEFT JOIN app.medical_billing_providers bp ON b.provider_id = bp.id
                  WHERE b.provider_id = @providerId AND b.bill_date::date = @billDate::date AND b.source = 'ai' AND b.category = 'pharmacy'
                  ORDER BY b.created_at DESC
                  LIMIT 1",
                MapBill,
                new { providerId, billDate });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to find same-day pharmacy bill for provider {ProviderId}", providerId);
            return null;
        }
    }

    public async Task AddToBillTotalAsync(long billId, decimal additionalAmount)
    {
        try
        {
            await db.ExecuteNonQueryAsync(
                "UPDATE app.medical_bills SET total_amount = total_amount + @amount, updated_at = @updatedAt WHERE id = @billId",
                new { billId, amount = additionalAmount, updatedAt = DateTime.UtcNow });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to add to bill total for bill {BillId}", billId);
        }
    }

    public async Task<bool> AddToProviderPaymentAmountAsync(long providerId, DateTime paymentDate, decimal additionalAmount)
    {
        try
        {
            var rows = await db.ExecuteNonQueryAsync(
                @"UPDATE app.medical_provider_payments
                  SET amount = amount + @amount
                  WHERE id = (
                      SELECT id FROM app.medical_provider_payments
                      WHERE provider_id = @providerId AND payment_date::date = @paymentDate::date AND source = 'ai'
                      ORDER BY created_at DESC LIMIT 1
                  )",
                new { providerId, paymentDate, amount = additionalAmount });
            return rows > 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to add to provider payment amount for provider {ProviderId}", providerId);
            return false;
        }
    }

    public async Task<MedicalDoctor?> FindOrCreateDoctorByNameAsync(long personId, string doctorName, MedicalAiService aiService)
    {
        try
        {
            // Step 1: Exact match
            var existing = await db.ExecuteReaderAsync(
                "SELECT id, name, specialty, phone, address, notes, created_at, updated_at FROM app.medical_doctors WHERE person_id = @personId AND LOWER(name) = LOWER(@name) LIMIT 1",
                reader => new MedicalDoctor
                {
                    Id = reader.GetInt64(0),
                    Name = reader.GetString(1),
                    Specialty = reader.IsDBNull(2) ? null : reader.GetString(2),
                    Phone = reader.IsDBNull(3) ? null : reader.GetString(3),
                    Address = reader.IsDBNull(4) ? null : reader.GetString(4),
                    Notes = reader.IsDBNull(5) ? null : reader.GetString(5),
                    CreatedAt = reader.GetDateTime(6),
                    UpdatedAt = reader.GetDateTime(7)
                },
                new { personId, name = doctorName });

            if (existing != null) return existing;

            // Step 2: AI fuzzy match
            var allDoctors = await GetDoctorsAsync(personId);
            if (allDoctors.Count > 0)
            {
                var existingNames = allDoctors.Select(d => d.Name).ToList();
                var matchedName = await aiService.FuzzyMatchDoctorAsync(doctorName, existingNames);
                if (matchedName != null)
                {
                    var matched = allDoctors.FirstOrDefault(d => string.Equals(d.Name, matchedName, StringComparison.OrdinalIgnoreCase));
                    if (matched != null) return matched;
                }
            }

            // Step 3: Create new
            return await CreateDoctorAsync(personId, doctorName);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to find or create doctor by name \"{DoctorName}\" for person {PersonId}", doctorName, personId);
            return null;
        }
    }

    public async Task AddPrescriptionFromAiAsync(long documentId, long personId, MedicalAiService.PrescriptionInfo rxInfo, string? doctorName, MedicalAiService aiService)
    {
        // Parse pickup date
        DateTime? pickupDate = null;
        if (!string.IsNullOrEmpty(rxInfo.PickupDate) && DateTime.TryParse(rxInfo.PickupDate, out var parsedDate))
            pickupDate = parsedDate;

        // Resolve doctor
        long? doctorId = null;
        var docName = rxInfo.DoctorName ?? doctorName;
        if (!string.IsNullOrWhiteSpace(docName))
        {
            var doctor = await FindOrCreateDoctorByNameAsync(personId, docName.Trim(), aiService);
            doctorId = doctor?.Id;
        }

        // Find or create prescription
        var existing = await FindActivePrescriptionByMedicationAsync(personId, rxInfo.MedicationName!);
        long prescriptionId;

        if (existing != null)
        {
            prescriptionId = existing.Id;
            // Update with new info if fields were previously null
            var needsUpdate = false;
            var updatedRxNumber = existing.RxNumber;
            var updatedDosage = existing.Dosage;
            var updatedFrequency = existing.Frequency;
            var updatedDoctorId = existing.DoctorId;

            if (!string.IsNullOrWhiteSpace(rxInfo.RxNumber) && rxInfo.RxNumber != existing.RxNumber)
            {
                updatedRxNumber = rxInfo.RxNumber;
                needsUpdate = true;
            }
            if (string.IsNullOrEmpty(existing.Dosage) && !string.IsNullOrEmpty(rxInfo.Dosage))
            {
                updatedDosage = rxInfo.Dosage;
                needsUpdate = true;
            }
            if (string.IsNullOrEmpty(existing.Frequency) && !string.IsNullOrEmpty(rxInfo.Frequency))
            {
                updatedFrequency = rxInfo.Frequency;
                needsUpdate = true;
            }
            if (!existing.DoctorId.HasValue && doctorId.HasValue)
            {
                updatedDoctorId = doctorId;
                needsUpdate = true;
            }

            if (needsUpdate)
            {
                await UpdatePrescriptionAsync(existing.Id, existing.MedicationName,
                    updatedDosage, updatedFrequency, updatedDoctorId,
                    existing.StartDate, existing.EndDate, existing.Notes, existing.IsActive, updatedRxNumber);
            }

            logger.LogInformation("Matched existing prescription {PrescriptionId} for medication \"{Medication}\"", prescriptionId, rxInfo.MedicationName);
        }
        else
        {
            var newRx = await CreatePrescriptionAsync(personId, rxInfo.MedicationName!,
                rxInfo.Dosage, rxInfo.Frequency, doctorId, pickupDate, null, rxInfo.RxNumber);
            if (newRx == null) return;
            prescriptionId = newRx.Id;
            logger.LogInformation("Created new prescription {PrescriptionId} for medication \"{Medication}\"", prescriptionId, rxInfo.MedicationName);
        }

        // Log pickup if date present and not already logged
        if (pickupDate.HasValue && !await HasPickupOnDateAsync(prescriptionId, pickupDate.Value))
        {
            await CreatePickupAsync(prescriptionId, pickupDate.Value,
                pharmacy: rxInfo.Pharmacy, cost: rxInfo.Copay, documentId: documentId);
            logger.LogInformation("Logged pickup for prescription {PrescriptionId} on {Date}", prescriptionId, pickupDate.Value);
        }

        // Create pharmacy billing if applicable
        if (!string.IsNullOrWhiteSpace(rxInfo.Pharmacy) && rxInfo.Copay is > 0 && pickupDate.HasValue)
        {
            var providerId = await FindOrCreateProviderAsync(personId, rxInfo.Pharmacy.Trim(), aiService);
            if (providerId == null) return;

            var existingBill = await FindSameDayPharmacyBillAsync(providerId.Value, pickupDate.Value);

            if (existingBill != null)
            {
                // Consolidate into existing same-day pharmacy bill
                await CreateChargeAsync(existingBill.Id, rxInfo.MedicationName!, rxInfo.Copay.Value, "ai");
                await AddToBillTotalAsync(existingBill.Id, rxInfo.Copay.Value);

                var paymentUpdated = await AddToProviderPaymentAmountAsync(providerId.Value, pickupDate.Value, rxInfo.Copay.Value);
                if (!paymentUpdated)
                    await CreateProviderPaymentAsync(providerId.Value, rxInfo.Copay.Value, pickupDate, $"Pharmacy - {rxInfo.MedicationName}", documentId, source: "ai");

                await LinkDocumentToBillAsync(existingBill.Id, documentId);
                logger.LogInformation("Consolidated prescription billing into existing bill {BillId} for provider {ProviderId}", existingBill.Id, providerId);
            }
            else
            {
                // Create new pharmacy bill
                var newBill = await CreateBillAsync(personId, rxInfo.Copay.Value,
                    $"Pharmacy - {rxInfo.MedicationName}", "pharmacy", pickupDate, providerId: providerId, source: "ai");
                if (newBill == null) return;

                await CreateChargeAsync(newBill.Id, rxInfo.MedicationName!, rxInfo.Copay.Value, "ai");
                await LinkDocumentToBillAsync(newBill.Id, documentId);
                await CreateProviderPaymentAsync(providerId.Value, rxInfo.Copay.Value, pickupDate, $"Pharmacy - {rxInfo.MedicationName}", documentId, source: "ai");
                logger.LogInformation("Created pharmacy bill {BillId} for prescription from provider {ProviderId}", newBill.Id, providerId);
            }
        }
    }

    // --- AI Processing Helpers ---

    public async Task UpdateAiResultsAsync(long docId, string? extractedText, string? classification, string aiRawResponse, long? doctorId = null)
    {
        try
        {
            await db.ExecuteNonQueryAsync(
                @"UPDATE app.medical_documents
                  SET extracted_text = @extractedText,
                      classification = @classification,
                      ai_processed = true,
                      ai_processed_at = @processedAt,
                      ai_raw_response = @aiRawResponse::jsonb,
                      doctor_id = @doctorId,
                      updated_at = @updatedAt
                  WHERE id = @docId",
                new
                {
                    docId,
                    extractedText,
                    classification,
                    aiRawResponse,
                    doctorId,
                    processedAt = DateTime.UtcNow,
                    updatedAt = DateTime.UtcNow
                });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to update AI results for document {DocumentId}", docId);
        }
    }

    public async Task AddTagsAsync(long docId, List<string> tagNames, string source = "ai")
    {
        try
        {
            foreach (var tagName in tagNames)
            {
                var normalizedName = tagName.Trim().ToLowerInvariant();
                if (string.IsNullOrEmpty(normalizedName)) continue;

                // Upsert tag
                var tagId = await db.ExecuteAsync<long>(
                    @"INSERT INTO app.medical_tags (name) VALUES (@name)
                      ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
                      RETURNING id",
                    new { name = normalizedName });

                // Link tag to document
                await db.ExecuteNonQueryAsync(
                    @"INSERT INTO app.medical_document_tags (document_id, tag_id, source)
                      VALUES (@documentId, @tagId, @source)
                      ON CONFLICT (document_id, tag_id) DO NOTHING",
                    new { documentId = docId, tagId, source });
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to add tags for document {DocumentId}", docId);
        }
    }

    // --- Billing Providers ---

    public async Task<List<MedicalBillingProvider>> GetProvidersAsync(long? personId = null, long? accessUserId = null)
    {
        try
        {
            var personFilter = personId.HasValue
                ? "p.person_id = @personId"
                : "p.person_id IN (SELECT person_id FROM app.medical_people_access WHERE user_id = @accessUserId)";

            return await db.ExecuteListReaderAsync(
                $@"SELECT p.id, p.name, p.person_id, p.notes, p.created_at, p.updated_at,
                         COALESCE(SUM(b.total_amount), 0) AS total_charged,
                         COALESCE((SELECT SUM(pp.amount) FROM app.medical_provider_payments pp WHERE pp.provider_id = p.id), 0) AS total_paid,
                         COUNT(DISTINCT b.id) AS bill_count
                  FROM app.medical_billing_providers p
                  LEFT JOIN app.medical_bills b ON b.provider_id = p.id
                  WHERE {personFilter}
                  GROUP BY p.id, p.name, p.person_id, p.notes, p.created_at, p.updated_at
                  ORDER BY p.name",
                reader => new MedicalBillingProvider
                {
                    Id = reader.GetInt64(0),
                    Name = reader.GetString(1),
                    PersonId = reader.GetInt64(2),
                    Notes = reader.IsDBNull(3) ? null : reader.GetString(3),
                    CreatedAt = reader.GetDateTime(4),
                    UpdatedAt = reader.GetDateTime(5),
                    TotalCharged = reader.GetDecimal(6),
                    TotalPaid = reader.GetDecimal(7),
                    BillCount = reader.GetInt32(8)
                },
                new { personId = personId ?? 0L, accessUserId = accessUserId ?? 0L });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get billing providers for person {PersonId}", personId);
            return [];
        }
    }

    public async Task<MedicalBillingProvider?> GetProviderByIdAsync(long providerId)
    {
        try
        {
            return await db.ExecuteReaderAsync(
                @"SELECT p.id, p.name, p.person_id, p.notes, p.created_at, p.updated_at,
                         COALESCE(SUM(b.total_amount), 0) AS total_charged,
                         COALESCE((SELECT SUM(pp.amount) FROM app.medical_provider_payments pp WHERE pp.provider_id = p.id), 0) AS total_paid,
                         COUNT(DISTINCT b.id) AS bill_count
                  FROM app.medical_billing_providers p
                  LEFT JOIN app.medical_bills b ON b.provider_id = p.id
                  WHERE p.id = @providerId
                  GROUP BY p.id, p.name, p.person_id, p.notes, p.created_at, p.updated_at",
                reader => new MedicalBillingProvider
                {
                    Id = reader.GetInt64(0),
                    Name = reader.GetString(1),
                    PersonId = reader.GetInt64(2),
                    Notes = reader.IsDBNull(3) ? null : reader.GetString(3),
                    CreatedAt = reader.GetDateTime(4),
                    UpdatedAt = reader.GetDateTime(5),
                    TotalCharged = reader.GetDecimal(6),
                    TotalPaid = reader.GetDecimal(7),
                    BillCount = reader.GetInt32(8)
                },
                new { providerId });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get billing provider {ProviderId}", providerId);
            return null;
        }
    }

    public async Task<MedicalBillingProvider?> CreateProviderAsync(long personId, string name, string? notes = null)
    {
        try
        {
            var now = DateTime.UtcNow;
            var id = await db.ExecuteAsync<long>(
                @"INSERT INTO app.medical_billing_providers (name, person_id, notes, created_at, updated_at)
                  VALUES (@name, @personId, @notes, @createdAt, @updatedAt)
                  RETURNING id",
                new { name, personId, notes, createdAt = now, updatedAt = now });

            return await GetProviderByIdAsync(id);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to create billing provider for person {PersonId}", personId);
            return null;
        }
    }

    public async Task<bool> UpdateProviderAsync(long id, string name, string? notes = null)
    {
        try
        {
            var rows = await db.ExecuteNonQueryAsync(
                @"UPDATE app.medical_billing_providers SET name = @name, notes = @notes, updated_at = @updatedAt WHERE id = @id",
                new { id, name, notes, updatedAt = DateTime.UtcNow });
            return rows > 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to update billing provider {ProviderId}", id);
            return false;
        }
    }

    public async Task<bool> DeleteProviderAsync(long id)
    {
        try
        {
            var rows = await db.ExecuteNonQueryAsync("DELETE FROM app.medical_billing_providers WHERE id = @id", new { id });
            return rows > 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to delete billing provider {ProviderId}", id);
            return false;
        }
    }

    public async Task<MedicalBillingProvider?> FindProviderByNameAsync(long personId, string name)
    {
        try
        {
            return await db.ExecuteReaderAsync(
                @"SELECT p.id, p.name, p.person_id, p.notes, p.created_at, p.updated_at,
                         COALESCE(SUM(b.total_amount), 0) AS total_charged,
                         COALESCE((SELECT SUM(pp.amount) FROM app.medical_provider_payments pp WHERE pp.provider_id = p.id), 0) AS total_paid,
                         COUNT(DISTINCT b.id) AS bill_count
                  FROM app.medical_billing_providers p
                  LEFT JOIN app.medical_bills b ON b.provider_id = p.id
                  WHERE p.person_id = @personId AND LOWER(p.name) = LOWER(@name)
                  GROUP BY p.id, p.name, p.person_id, p.notes, p.created_at, p.updated_at",
                reader => new MedicalBillingProvider
                {
                    Id = reader.GetInt64(0),
                    Name = reader.GetString(1),
                    PersonId = reader.GetInt64(2),
                    Notes = reader.IsDBNull(3) ? null : reader.GetString(3),
                    CreatedAt = reader.GetDateTime(4),
                    UpdatedAt = reader.GetDateTime(5),
                    TotalCharged = reader.GetDecimal(6),
                    TotalPaid = reader.GetDecimal(7),
                    BillCount = reader.GetInt32(8)
                },
                new { personId, name });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to find provider by name for person {PersonId}", personId);
            return null;
        }
    }

    public async Task<long?> FindOrCreateProviderAsync(long personId, string providerName, MedicalAiService aiService)
    {
        // Exact match first
        var existing = await FindProviderByNameAsync(personId, providerName);
        if (existing != null) return existing.Id;

        // AI fuzzy match
        try
        {
            var allProviders = await GetProvidersAsync(personId);
            if (allProviders.Count > 0)
            {
                var existingNames = allProviders.Select(p => p.Name).ToList();
                var matchedName = await aiService.FuzzyMatchProviderAsync(providerName, existingNames);
                if (matchedName != null)
                {
                    var matched = allProviders.FirstOrDefault(p => string.Equals(p.Name, matchedName, StringComparison.OrdinalIgnoreCase));
                    if (matched != null) return matched.Id;
                }
            }
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Fuzzy provider match failed, creating new provider");
        }

        // Create new
        var newProvider = await CreateProviderAsync(personId, providerName);
        return newProvider?.Id;
    }

    // --- Provider Payments ---

    public async Task<List<MedicalProviderPayment>> GetProviderPaymentsAsync(long providerId)
    {
        try
        {
            return await db.ExecuteListReaderAsync(
                @"SELECT id, provider_id, document_id, amount, payment_date, description, source, created_at
                  FROM app.medical_provider_payments
                  WHERE provider_id = @providerId
                  ORDER BY payment_date DESC NULLS LAST, created_at DESC",
                reader => new MedicalProviderPayment
                {
                    Id = reader.GetInt64(0),
                    ProviderId = reader.GetInt64(1),
                    DocumentId = reader.IsDBNull(2) ? null : reader.GetInt64(2),
                    Amount = reader.GetDecimal(3),
                    PaymentDate = reader.IsDBNull(4) ? null : reader.GetDateTime(4),
                    Description = reader.IsDBNull(5) ? null : reader.GetString(5),
                    Source = reader.GetString(6),
                    CreatedAt = reader.GetDateTime(7)
                },
                new { providerId });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get payments for provider {ProviderId}", providerId);
            return [];
        }
    }

    public async Task<MedicalProviderPayment?> CreateProviderPaymentAsync(long providerId, decimal amount, DateTime? paymentDate = null, string? description = null, long? documentId = null, string source = "manual")
    {
        try
        {
            return await db.ExecuteReaderAsync(
                @"INSERT INTO app.medical_provider_payments (provider_id, amount, payment_date, description, document_id, source, created_at)
                  VALUES (@providerId, @amount, @paymentDate, @description, @documentId, @source, @createdAt)
                  RETURNING id, provider_id, document_id, amount, payment_date, description, source, created_at",
                reader => new MedicalProviderPayment
                {
                    Id = reader.GetInt64(0),
                    ProviderId = reader.GetInt64(1),
                    DocumentId = reader.IsDBNull(2) ? null : reader.GetInt64(2),
                    Amount = reader.GetDecimal(3),
                    PaymentDate = reader.IsDBNull(4) ? null : reader.GetDateTime(4),
                    Description = reader.IsDBNull(5) ? null : reader.GetString(5),
                    Source = reader.GetString(6),
                    CreatedAt = reader.GetDateTime(7)
                },
                new { providerId, amount, paymentDate, description, documentId, source, createdAt = DateTime.UtcNow });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to create payment for provider {ProviderId}", providerId);
            return null;
        }
    }

    public async Task<bool> HasMatchingProviderPaymentAsync(long providerId, decimal amount, DateTime? paymentDate)
    {
        try
        {
            return await db.ExecuteReaderAsync(
                @"SELECT EXISTS (
                    SELECT 1 FROM app.medical_provider_payments
                    WHERE provider_id = @providerId
                      AND amount = @amount
                      AND source = 'ai'
                      AND ((@paymentDate IS NULL AND payment_date IS NULL)
                           OR payment_date = @paymentDate)
                )",
                reader => reader.GetBoolean(0),
                new { providerId, amount, paymentDate });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to check for matching payment on provider {ProviderId}", providerId);
            return false;
        }
    }

    public async Task<bool> DeleteProviderPaymentAsync(long id)
    {
        try
        {
            var rows = await db.ExecuteNonQueryAsync("DELETE FROM app.medical_provider_payments WHERE id = @id", new { id });
            return rows > 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to delete provider payment {PaymentId}", id);
            return false;
        }
    }

    // --- Bills ---

    public async Task<List<MedicalBill>> GetBillsAsync(long? personId = null, long? providerId = null, long? accessUserId = null)
    {
        try
        {
            var personFilter = personId.HasValue
                ? "b.person_id = @personId"
                : "b.person_id IN (SELECT person_id FROM app.medical_people_access WHERE user_id = @accessUserId)";
            var providerFilter = providerId.HasValue ? "AND b.provider_id = @providerId" : "";

            var query = $@"SELECT b.id, b.person_id, b.total_amount, b.summary, b.category, b.bill_date, b.doctor_id, b.provider_id, b.source, b.created_at, b.updated_at,
                                  d.name AS doctor_name,
                                  bp.name AS provider_name,
                                  (SELECT string_agg(COALESCE(md.title, md.file_name, 'Document #' || md.id::text), ', ')
                                   FROM app.medical_bill_documents bd
                                   JOIN app.medical_documents md ON bd.document_id = md.id
                                   WHERE bd.bill_id = b.id) AS document_names
                           FROM app.medical_bills b
                           LEFT JOIN app.medical_doctors d ON b.doctor_id = d.id
                           LEFT JOIN app.medical_billing_providers bp ON b.provider_id = bp.id
                           WHERE {personFilter} {providerFilter}
                           ORDER BY b.bill_date DESC NULLS LAST, b.created_at DESC";

            return await db.ExecuteListReaderAsync(query, MapBill, new { personId = personId ?? 0L, accessUserId = accessUserId ?? 0L, providerId });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get bills for person {PersonId}", personId);
            return [];
        }
    }

    public async Task<MedicalBill?> GetBillByIdAsync(long billId)
    {
        try
        {
            return await db.ExecuteReaderAsync(
                @"SELECT b.id, b.person_id, b.total_amount, b.summary, b.category, b.bill_date, b.doctor_id, b.provider_id, b.source, b.created_at, b.updated_at,
                         d.name AS doctor_name,
                         bp.name AS provider_name,
                         (SELECT string_agg(COALESCE(md.title, md.file_name, 'Document #' || md.id::text), ', ')
                          FROM app.medical_bill_documents bd
                          JOIN app.medical_documents md ON bd.document_id = md.id
                          WHERE bd.bill_id = b.id) AS document_names
                  FROM app.medical_bills b
                  LEFT JOIN app.medical_doctors d ON b.doctor_id = d.id
                  LEFT JOIN app.medical_billing_providers bp ON b.provider_id = bp.id
                  WHERE b.id = @billId",
                MapBill,
                new { billId });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get bill {BillId}", billId);
            return null;
        }
    }

    public async Task<MedicalBill?> CreateBillAsync(long personId, decimal totalAmount, string? summary = null, string? category = null, DateTime? billDate = null, long? doctorId = null, long? providerId = null, string source = "manual")
    {
        try
        {
            var now = DateTime.UtcNow;
            var id = await db.ExecuteAsync<long>(
                @"INSERT INTO app.medical_bills (person_id, total_amount, summary, category, bill_date, doctor_id, provider_id, source, created_at, updated_at)
                  VALUES (@personId, @totalAmount, @summary, @category, @billDate, @doctorId, @providerId, @source, @createdAt, @updatedAt)
                  RETURNING id",
                new { personId, totalAmount, summary, category, billDate, doctorId, providerId, source, createdAt = now, updatedAt = now });

            return await GetBillByIdAsync(id);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to create bill for person {PersonId}", personId);
            return null;
        }
    }

    public async Task<bool> UpdateBillAsync(long id, decimal totalAmount, string? summary = null, string? category = null, DateTime? billDate = null, long? doctorId = null, long? providerId = null)
    {
        try
        {
            var rows = await db.ExecuteNonQueryAsync(
                @"UPDATE app.medical_bills SET total_amount = @totalAmount, summary = @summary, category = @category, bill_date = @billDate, doctor_id = @doctorId, provider_id = @providerId, updated_at = @updatedAt
                  WHERE id = @id",
                new { id, totalAmount, summary, category, billDate, doctorId, providerId, updatedAt = DateTime.UtcNow });
            return rows > 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to update bill {BillId}", id);
            return false;
        }
    }

    public async Task<bool> DeleteBillAsync(long id)
    {
        try
        {
            var rows = await db.ExecuteNonQueryAsync("DELETE FROM app.medical_bills WHERE id = @id", new { id });
            return rows > 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to delete bill {BillId}", id);
            return false;
        }
    }

    public async Task<bool> LinkDocumentToBillAsync(long billId, long documentId)
    {
        try
        {
            await db.ExecuteNonQueryAsync(
                @"INSERT INTO app.medical_bill_documents (bill_id, document_id) VALUES (@billId, @documentId) ON CONFLICT (bill_id, document_id) DO NOTHING",
                new { billId, documentId });
            return true;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to link document {DocumentId} to bill {BillId}", documentId, billId);
            return false;
        }
    }

    public async Task<bool> UnlinkDocumentFromBillAsync(long billId, long documentId)
    {
        try
        {
            var rows = await db.ExecuteNonQueryAsync(
                "DELETE FROM app.medical_bill_documents WHERE bill_id = @billId AND document_id = @documentId",
                new { billId, documentId });
            return rows > 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to unlink document {DocumentId} from bill {BillId}", documentId, billId);
            return false;
        }
    }

    // --- Bill Summary ---

    public async Task<BillSummary> GetBillSummaryAsync(long? personId = null, long? accessUserId = null)
    {
        try
        {
            var personFilter = personId.HasValue
                ? "b.person_id = @personId"
                : "b.person_id IN (SELECT person_id FROM app.medical_people_access WHERE user_id = @accessUserId)";
            var providerPersonFilter = personId.HasValue
                ? "prov.person_id = @personId"
                : "prov.person_id IN (SELECT person_id FROM app.medical_people_access WHERE user_id = @accessUserId)";
            var filterParams = new { personId = personId ?? 0L, accessUserId = accessUserId ?? 0L };

            var totals = await db.ExecuteReaderAsync(
                $@"SELECT COALESCE(SUM(b.total_amount), 0),
                         COALESCE((SELECT SUM(pp.amount) FROM app.medical_provider_payments pp
                                   JOIN app.medical_billing_providers prov ON pp.provider_id = prov.id
                                   WHERE {providerPersonFilter}), 0)
                  FROM app.medical_bills b
                  WHERE {personFilter}",
                reader => new { Charged = reader.GetDecimal(0), TotalPaid = reader.GetDecimal(1) },
                filterParams);

            var byYear = await db.ExecuteListReaderAsync(
                $@"SELECT EXTRACT(YEAR FROM COALESCE(b.bill_date, b.created_at))::int AS year,
                         SUM(b.total_amount),
                         COUNT(b.id)
                  FROM app.medical_bills b
                  WHERE {personFilter}
                  GROUP BY EXTRACT(YEAR FROM COALESCE(b.bill_date, b.created_at))::int
                  ORDER BY year DESC",
                reader => new YearBreakdown
                {
                    Year = reader.GetInt32(0),
                    Total = reader.GetDecimal(1),
                    Count = reader.GetInt32(2)
                },
                filterParams);

            var byProvider = await db.ExecuteListReaderAsync(
                $@"SELECT COALESCE(prov.name, 'Unassigned'),
                         COALESCE(SUM(b.total_amount), 0),
                         COUNT(b.id)
                  FROM app.medical_bills b
                  LEFT JOIN app.medical_billing_providers prov ON b.provider_id = prov.id
                  WHERE {personFilter}
                  GROUP BY COALESCE(prov.name, 'Unassigned')
                  ORDER BY COALESCE(SUM(b.total_amount), 0) DESC",
                reader => new ProviderBreakdown
                {
                    ProviderName = reader.GetString(0),
                    Total = reader.GetDecimal(1),
                    Count = reader.GetInt32(2)
                },
                filterParams);

            var charged = totals?.Charged ?? 0;
            var totalPaid = totals?.TotalPaid ?? 0;

            return new BillSummary
            {
                TotalCharged = charged,
                TotalPaid = totalPaid,
                TotalDue = charged - totalPaid,
                ByYear = byYear,
                ByProvider = byProvider
            };
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get bill summary for person {PersonId}", personId);
            return new BillSummary();
        }
    }

    private static MedicalBill MapBill(Npgsql.NpgsqlDataReader reader)
    {
        return new MedicalBill
        {
            Id = reader.GetInt64(0),
            PersonId = reader.GetInt64(1),
            TotalAmount = reader.GetDecimal(2),
            Summary = reader.IsDBNull(3) ? null : reader.GetString(3),
            Category = reader.IsDBNull(4) ? null : reader.GetString(4),
            BillDate = reader.IsDBNull(5) ? null : reader.GetDateTime(5),
            DoctorId = reader.IsDBNull(6) ? null : reader.GetInt64(6),
            ProviderId = reader.IsDBNull(7) ? null : reader.GetInt64(7),
            Source = reader.GetString(8),
            CreatedAt = reader.GetDateTime(9),
            UpdatedAt = reader.GetDateTime(10),
            DoctorName = reader.IsDBNull(11) ? null : reader.GetString(11),
            ProviderName = reader.IsDBNull(12) ? null : reader.GetString(12),
            DocumentNames = reader.IsDBNull(13) ? null : reader.GetString(13)
        };
    }

    // --- Bill Charges ---

    public async Task<List<MedicalBillCharge>> GetChargesAsync(long billId)
    {
        try
        {
            return await db.ExecuteListReaderAsync(
                "SELECT id, bill_id, description, amount, source, created_at FROM app.medical_bill_charges WHERE bill_id = @billId ORDER BY created_at",
                reader => new MedicalBillCharge
                {
                    Id = reader.GetInt64(0),
                    BillId = reader.GetInt64(1),
                    Description = reader.GetString(2),
                    Amount = reader.GetDecimal(3),
                    Source = reader.GetString(4),
                    CreatedAt = reader.GetDateTime(5)
                },
                new { billId });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get charges for bill {BillId}", billId);
            return [];
        }
    }

    public async Task<MedicalBillCharge?> CreateChargeAsync(long billId, string description, decimal amount, string source = "manual")
    {
        try
        {
            return await db.ExecuteReaderAsync(
                @"INSERT INTO app.medical_bill_charges (bill_id, description, amount, source, created_at)
                  VALUES (@billId, @description, @amount, @source, @createdAt)
                  RETURNING id, bill_id, description, amount, source, created_at",
                reader => new MedicalBillCharge
                {
                    Id = reader.GetInt64(0),
                    BillId = reader.GetInt64(1),
                    Description = reader.GetString(2),
                    Amount = reader.GetDecimal(3),
                    Source = reader.GetString(4),
                    CreatedAt = reader.GetDateTime(5)
                },
                new { billId, description, amount, source, createdAt = DateTime.UtcNow });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to create charge for bill {BillId}", billId);
            return null;
        }
    }

    public async Task<bool> DeleteChargeAsync(long id)
    {
        try
        {
            var rows = await db.ExecuteNonQueryAsync("DELETE FROM app.medical_bill_charges WHERE id = @id", new { id });
            return rows > 0;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to delete charge {ChargeId}", id);
            return false;
        }
    }

    // --- Line-Item Bill Matching ---

    public async Task<LineItemMatchResult?> FindMatchingBillByLineItemsAsync(long personId, List<MedicalAiService.LineItemExtraction> newItems, long? providerId = null)
    {
        if (newItems.Count == 0) return null;

        var chargesByBill = await GetChargesGroupedByPersonAsync(personId, providerId);
        if (chargesByBill.Count == 0) return null;

        LineItemMatchResult? bestResult = null;

        foreach (var (billId, existingCharges) in chargesByBill)
        {
            var matchedCount = CountGreedyMatches(existingCharges, newItems);
            if (matchedCount == 0) continue;

            var isFullMatch = matchedCount == newItems.Count && matchedCount == existingCharges.Count;

            var result = new LineItemMatchResult
            {
                BillId = billId,
                IsFullMatch = isFullMatch,
                MatchedCount = matchedCount,
                ExistingCharges = existingCharges
            };

            if (bestResult == null
                || (result.IsFullMatch && !bestResult.IsFullMatch)
                || (result.IsFullMatch == bestResult.IsFullMatch && result.MatchedCount > bestResult.MatchedCount))
            {
                bestResult = result;
            }
        }

        return bestResult;
    }

    private async Task<Dictionary<long, List<MedicalBillCharge>>> GetChargesGroupedByPersonAsync(long personId, long? providerId = null)
    {
        var providerFilter = providerId.HasValue ? "AND b.provider_id = @providerId" : "";
        var charges = await db.ExecuteListReaderAsync(
            $@"SELECT c.id, c.bill_id, c.description, c.amount, c.source, c.created_at
              FROM app.medical_bill_charges c
              JOIN app.medical_bills b ON c.bill_id = b.id
              WHERE b.person_id = @personId {providerFilter}
              ORDER BY c.bill_id, c.created_at",
            reader => new MedicalBillCharge
            {
                Id = reader.GetInt64(0),
                BillId = reader.GetInt64(1),
                Description = reader.GetString(2),
                Amount = reader.GetDecimal(3),
                Source = reader.GetString(4),
                CreatedAt = reader.GetDateTime(5)
            },
            new { personId, providerId });

        var grouped = new Dictionary<long, List<MedicalBillCharge>>();
        foreach (var charge in charges)
        {
            if (!grouped.ContainsKey(charge.BillId))
                grouped[charge.BillId] = [];
            grouped[charge.BillId].Add(charge);
        }

        return grouped;
    }

    private static int CountGreedyMatches(List<MedicalBillCharge> existing, List<MedicalAiService.LineItemExtraction> newItems)
    {
        // Sort both by amount descending so distinctive large charges match first
        var existingPool = existing.OrderByDescending(c => c.Amount).ToList();
        var newSorted = newItems.OrderByDescending(i => i.Amount).ToList();

        var matched = 0;
        var usedExisting = new HashSet<int>();

        foreach (var newItem in newSorted)
        {
            var bestIdx = -1;
            var bestSimilarity = -1.0;

            for (var i = 0; i < existingPool.Count; i++)
            {
                if (usedExisting.Contains(i)) continue;

                // Primary signal: amount within $0.01
                if (Math.Abs(existingPool[i].Amount - newItem.Amount) > 0.01m) continue;

                // Tiebreaker: description word overlap
                var similarity = JaccardSimilarity(existingPool[i].Description, newItem.Description);
                if (bestIdx == -1 || similarity > bestSimilarity)
                {
                    bestIdx = i;
                    bestSimilarity = similarity;
                }
            }

            if (bestIdx >= 0)
            {
                usedExisting.Add(bestIdx);
                matched++;
            }
        }

        return matched;
    }

    private static double JaccardSimilarity(string? a, string? b)
    {
        if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b)) return 0;

        var wordsA = TokenizeWords(a);
        var wordsB = TokenizeWords(b);

        if (wordsA.Count == 0 || wordsB.Count == 0) return 0;

        var intersection = wordsA.Intersect(wordsB).Count();
        var union = wordsA.Union(wordsB).Count();

        return union == 0 ? 0 : (double)intersection / union;
    }

    private static HashSet<string> TokenizeWords(string text)
    {
        // Lowercase, strip punctuation, split on whitespace
        var cleaned = new string(text.ToLowerInvariant().Select(c => char.IsLetterOrDigit(c) || c == ' ' ? c : ' ').ToArray());
        return cleaned.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToHashSet();
    }

    // --- AI Bill Helpers ---

    public async Task CleanupAiBillsForDocumentAsync(long documentId)
    {
        try
        {
            // Delete AI-sourced provider payments for this document
            await db.ExecuteNonQueryAsync(
                "DELETE FROM app.medical_provider_payments WHERE document_id = @documentId AND source = 'ai'",
                new { documentId });

            // Find all AI-sourced bills linked to this document
            var billIds = await db.ExecuteListReaderAsync(
                @"SELECT bd.bill_id FROM app.medical_bill_documents bd
                  JOIN app.medical_bills b ON bd.bill_id = b.id
                  WHERE bd.document_id = @documentId AND b.source = 'ai'",
                reader => reader.GetInt64(0),
                new { documentId });

            foreach (var billId in billIds)
            {
                // Unlink document from bill
                await db.ExecuteNonQueryAsync(
                    "DELETE FROM app.medical_bill_documents WHERE bill_id = @billId AND document_id = @documentId",
                    new { billId, documentId });

                // Delete AI-sourced charges on this bill
                await db.ExecuteNonQueryAsync(
                    "DELETE FROM app.medical_bill_charges WHERE bill_id = @billId AND source = 'ai'",
                    new { billId });

                // Delete the bill if it has no remaining document links
                var linkCount = await db.ExecuteAsync<long>(
                    "SELECT COUNT(*) FROM app.medical_bill_documents WHERE bill_id = @billId",
                    new { billId });

                if (linkCount == 0)
                {
                    await db.ExecuteNonQueryAsync(
                        "DELETE FROM app.medical_bill_payments WHERE bill_id = @billId",
                        new { billId });
                    await db.ExecuteNonQueryAsync(
                        "DELETE FROM app.medical_bills WHERE id = @billId AND source = 'ai'",
                        new { billId });
                }
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to cleanup AI bills for document {DocumentId}", documentId);
        }
    }

    public async Task<MedicalBill?> FindMatchingBillAsync(long personId, decimal totalAmount, string? category, DateTime? billDate, long? providerId = null)
    {
        try
        {
            // Pass 1: strict — exact amount + category + date
            var match = await FindBillByConditionsAsync(personId, totalAmount, category, billDate, providerId);
            if (match != null) return match;

            // Pass 2: drop category (AI may classify same doc differently)
            if (!string.IsNullOrEmpty(category))
            {
                match = await FindBillByConditionsAsync(personId, totalAmount, null, billDate, providerId);
                if (match != null) return match;
            }

            // Pass 3: exact amount only (no category or date)
            if (billDate.HasValue)
            {
                match = await FindBillByConditionsAsync(personId, totalAmount, null, null, providerId);
                if (match != null) return match;
            }

            return null;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to find matching bill for person {PersonId}", personId);
            return null;
        }
    }

    private async Task<MedicalBill?> FindBillByConditionsAsync(long personId, decimal totalAmount, string? category, DateTime? billDate, long? providerId = null)
    {
        var conditions = new List<string> { "b.person_id = @personId", "b.total_amount = @totalAmount" };

        if (!string.IsNullOrEmpty(category))
            conditions.Add("b.category = @category");

        if (billDate.HasValue)
            conditions.Add("b.bill_date IS NOT NULL AND ABS(EXTRACT(EPOCH FROM (b.bill_date - @billDate::timestamp)) / 86400) <= 30");

        if (providerId.HasValue)
            conditions.Add("b.provider_id = @providerId");

        var whereClause = string.Join(" AND ", conditions);

        return await db.ExecuteReaderAsync(
            $@"SELECT b.id, b.person_id, b.total_amount, b.summary, b.category, b.bill_date, b.doctor_id, b.provider_id, b.source, b.created_at, b.updated_at,
                      d.name AS doctor_name,
                      prov.name AS provider_name,
                      (SELECT string_agg(COALESCE(md.title, md.file_name, 'Document #' || md.id::text), ', ')
                       FROM app.medical_bill_documents bd2
                       JOIN app.medical_documents md ON bd2.document_id = md.id
                       WHERE bd2.bill_id = b.id) AS document_names
               FROM app.medical_bills b
               LEFT JOIN app.medical_doctors d ON b.doctor_id = d.id
               LEFT JOIN app.medical_billing_providers prov ON b.provider_id = prov.id
               WHERE {whereClause}
               ORDER BY b.created_at DESC
               LIMIT 1",
            MapBill,
            new { personId, totalAmount, category, billDate, providerId });
    }

    public async Task AddBillsFromAiAsync(long documentId, long personId, List<MedicalAiService.BillExtraction> bills, List<MedicalAiService.PaymentExtraction>? payments, MedicalAiService aiService)
    {
        try
        {
            var billIndexToId = new Dictionary<int, long>();
            var billIndexToProvider = new Dictionary<int, long>();

            for (var i = 0; i < bills.Count; i++)
            {
                var bill = bills[i];
                if (bill.TotalAmount <= 0) continue;

                DateTime? billDate = null;
                if (!string.IsNullOrEmpty(bill.BillDate) && DateTime.TryParse(bill.BillDate, out var parsed))
                    billDate = parsed;

                // Resolve provider
                long? providerId = null;
                if (!string.IsNullOrWhiteSpace(bill.ProviderName))
                {
                    providerId = await FindOrCreateProviderAsync(personId, bill.ProviderName.Trim(), aiService);
                }

                long? matchedBillId = null;

                // Phase 1: Line-item matching (if bill has line items)
                if (bill.LineItems is { Count: > 0 })
                {
                    // Try scoped to provider first, then unscoped
                    var lineItemMatch = await FindMatchingBillByLineItemsAsync(personId, bill.LineItems, providerId);
                    if (lineItemMatch == null && providerId.HasValue)
                        lineItemMatch = await FindMatchingBillByLineItemsAsync(personId, bill.LineItems);

                    if (lineItemMatch != null)
                    {
                        if (lineItemMatch.IsFullMatch)
                        {
                            matchedBillId = lineItemMatch.BillId;
                            logger.LogInformation("Line-item full match: doc {DocumentId} bill #{Index} → existing bill {BillId}", documentId, i, lineItemMatch.BillId);
                        }
                        else
                        {
                            // Partial match — ask AI to disambiguate
                            var isSame = await aiService.DisambiguateBillMatchAsync(lineItemMatch.ExistingCharges, bill.LineItems, bill.Summary);
                            if (isSame)
                            {
                                matchedBillId = lineItemMatch.BillId;
                                logger.LogInformation("Line-item partial match confirmed by AI: doc {DocumentId} bill #{Index} → existing bill {BillId}", documentId, i, lineItemMatch.BillId);
                            }
                        }
                    }
                }

                // Phase 2: Amount fallback — scoped to provider, then unscoped
                if (!matchedBillId.HasValue)
                {
                    var existingBill = await FindMatchingBillAsync(personId, bill.TotalAmount, bill.Category, billDate, providerId);
                    if (existingBill == null && providerId.HasValue)
                        existingBill = await FindMatchingBillAsync(personId, bill.TotalAmount, bill.Category, billDate);
                    if (existingBill != null)
                        matchedBillId = existingBill.Id;
                }

                // Phase 3: Create new bill
                long billId;
                var isNewBill = false;
                if (matchedBillId.HasValue)
                {
                    billId = matchedBillId.Value;
                }
                else
                {
                    var newBill = await CreateBillAsync(personId, bill.TotalAmount, bill.Summary, bill.Category, billDate, providerId: providerId, source: "ai");
                    if (newBill == null) continue;
                    billId = newBill.Id;
                    isNewBill = true;
                }

                await LinkDocumentToBillAsync(billId, documentId);
                billIndexToId[i] = billId;
                if (providerId.HasValue)
                    billIndexToProvider[i] = providerId.Value;

                // Only create charges for new bills to avoid duplicates
                if (isNewBill && bill.LineItems is { Count: > 0 })
                {
                    foreach (var item in bill.LineItems)
                    {
                        if (item.Amount > 0 && !string.IsNullOrWhiteSpace(item.Description))
                            await CreateChargeAsync(billId, item.Description, item.Amount, "ai");
                    }
                }
            }

            if (payments == null) return;

            foreach (var payment in payments)
            {
                if (payment.Amount <= 0) continue;

                // Determine target provider from bill index
                long? targetProviderId = null;
                if (payment.BillIndex.HasValue && billIndexToProvider.TryGetValue(payment.BillIndex.Value, out var mappedProviderId))
                {
                    targetProviderId = mappedProviderId;
                }
                else if (billIndexToProvider.Count == 1)
                {
                    targetProviderId = billIndexToProvider.Values.First();
                }

                if (!targetProviderId.HasValue) continue;

                DateTime? paymentDate = null;
                if (!string.IsNullOrEmpty(payment.PaymentDate) && DateTime.TryParse(payment.PaymentDate, out var parsedDate))
                    paymentDate = parsedDate;

                // Skip if a matching AI payment already exists (dedup for re-uploaded receipts)
                var isDuplicate = await HasMatchingProviderPaymentAsync(
                    targetProviderId.Value, payment.Amount, paymentDate);
                if (isDuplicate) continue;

                await CreateProviderPaymentAsync(targetProviderId.Value, payment.Amount, paymentDate, payment.Description, documentId, source: "ai");
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to add AI bills for document {DocumentId}", documentId);
        }
    }

    // --- AI Condition Assignment ---

    public async Task AssignConditionsFromAiAsync(long documentId, long personId, List<string> conditionNames, MedicalAiService aiService)
    {
        foreach (var name in conditionNames)
        {
            if (string.IsNullOrWhiteSpace(name)) continue;

            try
            {
                var condition = await FindOrCreateConditionByNameAsync(personId, name.Trim(), aiService);
                if (condition != null)
                {
                    await LinkDocumentToConditionAsync(documentId, condition.Id);
                    logger.LogInformation("Linked condition \"{ConditionName}\" (ID {ConditionId}) to document {DocumentId}",
                        condition.Name, condition.Id, documentId);
                }
            }
            catch (Exception ex)
            {
                logger.LogWarning(ex, "Failed to assign condition \"{ConditionName}\" to document {DocumentId}", name, documentId);
            }
        }
    }

    public async Task<MedicalCondition?> FindOrCreateConditionByNameAsync(long personId, string conditionName, MedicalAiService aiService)
    {
        // Exact case-insensitive match
        try
        {
            var existing = await db.ExecuteReaderAsync(
                "SELECT id, person_id, name, diagnosed_date, notes, is_active, created_at, updated_at FROM app.medical_conditions WHERE person_id = @personId AND LOWER(name) = LOWER(@name) LIMIT 1",
                reader => new MedicalCondition
                {
                    Id = reader.GetInt64(0),
                    PersonId = reader.GetInt64(1),
                    Name = reader.GetString(2),
                    DiagnosedDate = reader.IsDBNull(3) ? null : reader.GetDateTime(3),
                    Notes = reader.IsDBNull(4) ? null : reader.GetString(4),
                    IsActive = reader.GetBoolean(5),
                    CreatedAt = reader.GetDateTime(6),
                    UpdatedAt = reader.GetDateTime(7)
                },
                new { personId, name = conditionName });

            if (existing != null) return existing;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to find condition by name for person {PersonId}", personId);
        }

        // AI fuzzy match against existing conditions
        try
        {
            var allConditions = await GetConditionsAsync(personId);
            if (allConditions.Count > 0)
            {
                var existingNames = allConditions.Select(c => c.Name).ToList();
                var matchedName = await aiService.FuzzyMatchConditionAsync(conditionName, existingNames);
                if (matchedName != null)
                {
                    var matched = allConditions.FirstOrDefault(c => string.Equals(c.Name, matchedName, StringComparison.OrdinalIgnoreCase));
                    if (matched != null) return matched;
                }
            }
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Fuzzy condition match failed, creating new condition");
        }

        // Create new
        return await CreateConditionAsync(personId, conditionName);
    }

    public async Task LinkDocumentToConditionAsync(long documentId, long conditionId)
    {
        try
        {
            await db.ExecuteNonQueryAsync(
                @"INSERT INTO app.medical_document_conditions (document_id, condition_id)
                  VALUES (@documentId, @conditionId)
                  ON CONFLICT DO NOTHING",
                new { documentId, conditionId });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to link document {DocumentId} to condition {ConditionId}", documentId, conditionId);
        }
    }

    public async Task<List<long>> GetUnprocessedDocumentIdsAsync()
    {
        try
        {
            return await db.ExecuteListReaderAsync(
                "SELECT id FROM app.medical_documents WHERE ai_processed = false ORDER BY created_at",
                reader => reader.GetInt64(0));
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get unprocessed document IDs");
            return [];
        }
    }

    public async Task<List<MedicalTag>> GetDocumentTagsAsync(long documentId)
    {
        try
        {
            return await db.ExecuteListReaderAsync(
                @"SELECT t.id, t.name, t.created_at
                  FROM app.medical_tags t
                  JOIN app.medical_document_tags dt ON dt.tag_id = t.id
                  WHERE dt.document_id = @documentId
                  ORDER BY t.name",
                reader => new MedicalTag
                {
                    Id = reader.GetInt64(0),
                    Name = reader.GetString(1),
                    CreatedAt = reader.GetDateTime(2)
                },
                new { documentId });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get tags for document {DocumentId}", documentId);
            return [];
        }
    }

    // --- Timeline ---

    public async Task<List<TimelineEvent>> GetTimelineAsync(long? personId = null, long? accessUserId = null, int offset = 0, int limit = 100)
    {
        try
        {
            var personFilter = personId.HasValue
                ? "= @personId"
                : "IN (SELECT person_id FROM app.medical_people_access WHERE user_id = @accessUserId)";

            return await db.ExecuteListReaderAsync(
                $@"SELECT event_type, id, person_id, label, detail, sub_type, event_date, doctor_id, created_at
                  FROM (
                      SELECT 'document' AS event_type, d.id, d.person_id, COALESCE(d.title, d.file_name) AS label, d.description AS detail,
                             d.classification AS sub_type, d.document_date AS event_date, d.doctor_id, d.created_at
                      FROM app.medical_documents d WHERE d.person_id {personFilter}
                      UNION ALL
                      SELECT 'condition', c.id, c.person_id, c.name, c.notes,
                             CASE WHEN c.is_active THEN 'active' ELSE 'resolved' END, c.diagnosed_date, NULL, c.created_at
                      FROM app.medical_conditions c WHERE c.person_id {personFilter}
                      UNION ALL
                      SELECT 'prescription', p.id, p.person_id, p.medication_name, CONCAT_WS(' - ', p.dosage, p.frequency),
                             CASE WHEN p.is_active THEN 'active' ELSE 'ended' END, p.start_date, p.doctor_id, p.created_at
                      FROM app.medical_prescriptions p WHERE p.person_id {personFilter}
                      UNION ALL
                      SELECT 'bill', b.id, b.person_id, b.summary, bp.name,
                             b.category, b.bill_date, b.doctor_id, b.created_at
                      FROM app.medical_bills b
                      LEFT JOIN app.medical_billing_providers bp ON b.provider_id = bp.id
                      WHERE b.person_id {personFilter}
                  ) AS timeline
                  ORDER BY COALESCE(event_date, created_at) DESC
                  LIMIT @limit OFFSET @offset",
                reader => new TimelineEvent
                {
                    EventType = reader.GetString(0),
                    Id = reader.GetInt64(1),
                    PersonId = reader.GetInt64(2),
                    Label = reader.IsDBNull(3) ? null : reader.GetString(3),
                    Detail = reader.IsDBNull(4) ? null : reader.GetString(4),
                    SubType = reader.IsDBNull(5) ? null : reader.GetString(5),
                    EventDate = reader.IsDBNull(6) ? null : reader.GetDateTime(6),
                    DoctorId = reader.IsDBNull(7) ? null : reader.GetInt64(7),
                    CreatedAt = reader.GetDateTime(8)
                },
                new { personId = personId ?? 0L, accessUserId = accessUserId ?? 0L, limit, offset });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get timeline for person {PersonId}", personId);
            return [];
        }
    }

    // --- Visit Prep ---

    public async Task<VisitPrepData> GetVisitPrepAsync(long personId, long doctorId)
    {
        var data = new VisitPrepData();

        try
        {
            data.RecentDocuments = await db.ExecuteListReaderAsync(
                @"SELECT id, title, file_name, document_date, classification
                  FROM app.medical_documents
                  WHERE person_id = @personId AND doctor_id = @doctorId
                  ORDER BY COALESCE(document_date, created_at) DESC LIMIT 10",
                reader => new VisitPrepDocument
                {
                    Id = reader.GetInt64(0),
                    Title = reader.IsDBNull(1) ? null : reader.GetString(1),
                    FileName = reader.IsDBNull(2) ? null : reader.GetString(2),
                    DocumentDate = reader.IsDBNull(3) ? null : reader.GetDateTime(3),
                    Classification = reader.IsDBNull(4) ? null : reader.GetString(4)
                },
                new { personId, doctorId });

            data.ActiveConditions = await db.ExecuteListReaderAsync(
                "SELECT id, person_id, name, diagnosed_date, notes, is_active, created_at, updated_at FROM app.medical_conditions WHERE person_id = @personId AND is_active = true ORDER BY name",
                reader => new MedicalCondition
                {
                    Id = reader.GetInt64(0),
                    PersonId = reader.GetInt64(1),
                    Name = reader.GetString(2),
                    DiagnosedDate = reader.IsDBNull(3) ? null : reader.GetDateTime(3),
                    Notes = reader.IsDBNull(4) ? null : reader.GetString(4),
                    IsActive = reader.GetBoolean(5),
                    CreatedAt = reader.GetDateTime(6),
                    UpdatedAt = reader.GetDateTime(7)
                },
                new { personId });

            data.ActivePrescriptions = await db.ExecuteListReaderAsync(
                @"SELECT p.id, p.person_id, p.doctor_id, p.medication_name, p.dosage, p.frequency, p.rx_number,
                         p.is_active, p.start_date, p.end_date, p.notes, p.created_at, p.updated_at,
                         d.name AS doctor_name
                  FROM app.medical_prescriptions p
                  LEFT JOIN app.medical_doctors d ON p.doctor_id = d.id
                  WHERE p.person_id = @personId AND p.is_active = true
                  ORDER BY p.medication_name",
                reader => new MedicalPrescription
                {
                    Id = reader.GetInt64(0),
                    PersonId = reader.GetInt64(1),
                    DoctorId = reader.IsDBNull(2) ? null : reader.GetInt64(2),
                    MedicationName = reader.GetString(3),
                    Dosage = reader.IsDBNull(4) ? null : reader.GetString(4),
                    Frequency = reader.IsDBNull(5) ? null : reader.GetString(5),
                    RxNumber = reader.IsDBNull(6) ? null : reader.GetString(6),
                    IsActive = reader.GetBoolean(7),
                    StartDate = reader.IsDBNull(8) ? null : reader.GetDateTime(8),
                    EndDate = reader.IsDBNull(9) ? null : reader.GetDateTime(9),
                    Notes = reader.IsDBNull(10) ? null : reader.GetString(10),
                    CreatedAt = reader.GetDateTime(11),
                    UpdatedAt = reader.GetDateTime(12),
                    DoctorName = reader.IsDBNull(13) ? null : reader.GetString(13)
                },
                new { personId });

            data.RecentBills = await db.ExecuteListReaderAsync(
                @"SELECT b.id, b.total_amount, b.summary, b.category, b.bill_date
                  FROM app.medical_bills b
                  WHERE b.person_id = @personId AND b.doctor_id = @doctorId
                    AND b.bill_date >= NOW() - INTERVAL '6 months'
                  ORDER BY b.bill_date DESC NULLS LAST LIMIT 10",
                reader => new VisitPrepBill
                {
                    Id = reader.GetInt64(0),
                    TotalAmount = reader.GetDecimal(1),
                    Summary = reader.IsDBNull(2) ? null : reader.GetString(2),
                    Category = reader.IsDBNull(3) ? null : reader.GetString(3),
                    BillDate = reader.IsDBNull(4) ? null : reader.GetDateTime(4)
                },
                new { personId, doctorId });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to get visit prep data for person {PersonId}, doctor {DoctorId}", personId, doctorId);
        }

        return data;
    }

    private static MedicalDocument MapDocument(Npgsql.NpgsqlDataReader reader)
    {
        return new MedicalDocument
        {
            Id = reader.GetInt64(0),
            PersonId = reader.GetInt64(1),
            DocumentType = reader.GetString(2),
            FileName = reader.IsDBNull(3) ? null : reader.GetString(3),
            FilePath = reader.IsDBNull(4) ? null : reader.GetString(4),
            FileSize = reader.IsDBNull(5) ? null : reader.GetInt64(5),
            MimeType = reader.IsDBNull(6) ? null : reader.GetString(6),
            IsEncrypted = reader.GetBoolean(7),
            Title = reader.IsDBNull(8) ? null : reader.GetString(8),
            Description = reader.IsDBNull(9) ? null : reader.GetString(9),
            DocumentDate = reader.IsDBNull(10) ? null : reader.GetDateTime(10),
            Classification = reader.IsDBNull(11) ? null : reader.GetString(11),
            ExtractedText = reader.IsDBNull(12) ? null : reader.GetString(12),
            AiProcessed = reader.GetBoolean(13),
            AiProcessedAt = reader.IsDBNull(14) ? null : reader.GetDateTime(14),
            DoctorId = reader.IsDBNull(15) ? null : reader.GetInt64(15),
            CreatedAt = reader.GetDateTime(16),
            UpdatedAt = reader.GetDateTime(17)
        };
    }
}

public class BillSummary
{
    public decimal TotalCharged { get; set; }
    public decimal TotalPaid { get; set; }
    public decimal TotalDue { get; set; }
    public List<YearBreakdown> ByYear { get; set; } = [];
    public List<ProviderBreakdown> ByProvider { get; set; } = [];
}

public class YearBreakdown
{
    public int Year { get; set; }
    public decimal Total { get; set; }
    public int Count { get; set; }
}

public class ProviderBreakdown
{
    public string ProviderName { get; set; } = "";
    public decimal Total { get; set; }
    public int Count { get; set; }
}

public class LineItemMatchResult
{
    public long BillId { get; set; }
    public bool IsFullMatch { get; set; }
    public int MatchedCount { get; set; }
    public List<MedicalBillCharge> ExistingCharges { get; set; } = [];
}

