using System.Data;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using ApiDenuncias.Configuration;
using GestionaDenuncias.Shared.Models;
using Microsoft.Extensions.Options;
using MySqlConnector;

namespace ApiDenuncias.Services;

public sealed class MySqlDenunciaStore : IDenunciaStore
{
    private const int AttachmentChunkSizeBytes = 8 * 1024 * 1024;
    private const int AttachmentChunkThresholdBytes = 32 * 1024 * 1024;

    private static readonly byte[] ChunkedAttachmentPrefix = Encoding.ASCII.GetBytes("chunked:v1:");

    private const string AttachmentChunksTableSql = """
        CREATE TABLE IF NOT EXISTS complaint_attachment_chunks (
            attachment_id BIGINT NOT NULL,
            chunk_index INT NOT NULL,
            content LONGBLOB NOT NULL,
            created_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
            PRIMARY KEY (attachment_id, chunk_index),
            CONSTRAINT fk_attachment_chunks_attachment
                FOREIGN KEY (attachment_id) REFERENCES complaint_attachments(id)
                ON DELETE CASCADE
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
        """;

    private const string GestionaUploadHistoryTableSql = """
        CREATE TABLE IF NOT EXISTS gestiona_upload_history (
            id BIGINT NOT NULL AUTO_INCREMENT,
            external_report_id INT NOT NULL,
            gestiona_file_url TEXT NOT NULL,
            gestiona_file_code VARCHAR(128) NOT NULL DEFAULT '',
            upload_type VARCHAR(64) NOT NULL DEFAULT '',
            assigned_group_code VARCHAR(32) NOT NULL DEFAULT '',
            uploaded_by_username VARCHAR(256) NOT NULL DEFAULT '',
            uploaded_at_utc DATETIME(6) NOT NULL,
            subject TEXT NULL,
            document_names TEXT NULL,
            created_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
            PRIMARY KEY (id),
            KEY ix_gestiona_upload_history_report (external_report_id),
            KEY ix_gestiona_upload_history_uploaded_at (uploaded_at_utc),
            KEY ix_gestiona_upload_history_user (uploaded_by_username)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
        """;

    private const string ComplaintSelectColumns = """
                external_registry_id,
                external_report_id,
                report_date_utc,
                gestiona_file_url,
                gestiona_file_code,
                gestiona_person_id,
                tag,
                status_name,
                complaint_type,
                reporter_kind,
                is_legal_entity,
                reporter_first_name,
                reporter_first_surname,
                reporter_second_surname,
                reporter_last_name,
                reporter_business_name,
                reporter_gender,
                reporter_document_id,
                reporter_document_type,
                reporter_origin_country,
                subject,
                accused_party,
                accused_party_details,
                complaint_description,
                reported_to_institution,
                reported_institution_details,
                requested_protection,
                requested_protection_details,
                information_mode,
                facts_location,
                facts_date_utc,
                forwarding_authorization,
                forwarding_personal_data_preference,
                notification_preference,
                electronic_notification,
                online_tracking_preference,
                postal_notification_preference,
                email,
                sms_notification,
                accepted_terms,
                comments,
                phone,
                address_line,
                address_road_type,
                address_number,
                address_floor,
                address_door,
                address_block,
                address_stair,
                address_extra,
                municipality,
                province,
                postal_code,
                country_code,
                form_fields_json,
                raw_report_text,
                is_confidential,
                is_update,
                gestiona_procedure_id,
                gestiona_group_id,
                display_name,
                workflow_status,
                selected_document_name,
                gestiona_uploaded_at_utc,
                gestiona_last_upload_type,
                gestiona_assigned_group,
                pending_update_source,
                is_in_gestiona,
                is_rejected,
                key_date,
                encryption_scheme,
                encrypted_at_utc
            """;

    private const string AttachmentSelectColumns = """
                a.id,
                a.attachment_type_id,
                a.description,
                a.attachment_date_utc,
                a.notes,
                c.external_report_id,
                a.original_file_name,
                a.content,
                a.content_sha256,
                a.uploaded_to_gestiona,
                a.uploaded_at_utc,
                a.key_date,
                a.encryption_scheme,
                a.encrypted_at_utc
            """;

    private const string AttachmentMetadataSelectColumns = """
                a.id,
                a.attachment_type_id,
                a.description,
                a.attachment_date_utc,
                a.notes,
                c.external_report_id,
                a.original_file_name,
                NULL AS content,
                a.content_sha256,
                a.uploaded_to_gestiona,
                a.uploaded_at_utc,
                a.key_date,
                a.encryption_scheme,
                a.encrypted_at_utc
            """;

    private static readonly (string Table, string Column, string Definition)[] SchemaColumnsToEnsure =
    [
        ("complaints", "gestiona_file_code", "`gestiona_file_code` VARCHAR(128) NOT NULL DEFAULT ''"),
        ("complaints", "reporter_kind", "`reporter_kind` VARCHAR(128) NOT NULL DEFAULT ''"),
        ("complaints", "is_legal_entity", "`is_legal_entity` TINYINT(1) NOT NULL DEFAULT 0"),
        ("complaints", "reporter_first_surname", "`reporter_first_surname` VARCHAR(256) NOT NULL DEFAULT ''"),
        ("complaints", "reporter_second_surname", "`reporter_second_surname` VARCHAR(256) NOT NULL DEFAULT ''"),
        ("complaints", "reporter_business_name", "`reporter_business_name` VARCHAR(256) NOT NULL DEFAULT ''"),
        ("complaints", "reporter_document_type", "`reporter_document_type` VARCHAR(128) NOT NULL DEFAULT ''"),
        ("complaints", "reporter_origin_country", "`reporter_origin_country` VARCHAR(128) NOT NULL DEFAULT ''"),
        ("complaints", "accused_party_details", "`accused_party_details` VARCHAR(512) NOT NULL DEFAULT ''"),
        ("complaints", "reported_institution_details", "`reported_institution_details` VARCHAR(512) NOT NULL DEFAULT ''"),
        ("complaints", "requested_protection", "`requested_protection` VARCHAR(128) NOT NULL DEFAULT ''"),
        ("complaints", "requested_protection_details", "`requested_protection_details` VARCHAR(512) NOT NULL DEFAULT ''"),
        ("complaints", "forwarding_authorization", "`forwarding_authorization` VARCHAR(128) NOT NULL DEFAULT ''"),
        ("complaints", "forwarding_personal_data_preference", "`forwarding_personal_data_preference` VARCHAR(256) NOT NULL DEFAULT ''"),
        ("complaints", "online_tracking_preference", "`online_tracking_preference` VARCHAR(256) NOT NULL DEFAULT ''"),
        ("complaints", "postal_notification_preference", "`postal_notification_preference` VARCHAR(256) NOT NULL DEFAULT ''"),
        ("complaints", "address_road_type", "`address_road_type` VARCHAR(32) NOT NULL DEFAULT ''"),
        ("complaints", "address_extra", "`address_extra` VARCHAR(256) NOT NULL DEFAULT ''"),
        ("complaints", "form_fields_json", "`form_fields_json` LONGTEXT NULL"),
        ("complaints", "raw_report_text", "`raw_report_text` LONGTEXT NULL"),
        ("complaints", "key_date", "`key_date` DATE NULL"),
        ("complaints", "encryption_scheme", "`encryption_scheme` VARCHAR(64) NOT NULL DEFAULT 'none'"),
        ("complaints", "encrypted_at_utc", "`encrypted_at_utc` DATETIME(6) NULL"),
        ("complaints", "gestiona_last_upload_type", "`gestiona_last_upload_type` VARCHAR(64) NOT NULL DEFAULT ''"),
        ("complaints", "gestiona_assigned_group", "`gestiona_assigned_group` VARCHAR(255) NOT NULL DEFAULT ''"),
        ("complaints", "pending_update_source", "`pending_update_source` VARCHAR(256) NOT NULL DEFAULT ''"),
        ("inbox_reports", "owner_user_id", "`owner_user_id` BIGINT NULL"),
        ("complaint_attachments", "content_sha256", "`content_sha256` CHAR(64) NOT NULL DEFAULT ''"),
        ("complaint_attachments", "key_date", "`key_date` DATE NULL"),
        ("complaint_attachments", "encryption_scheme", "`encryption_scheme` VARCHAR(64) NOT NULL DEFAULT 'none'"),
        ("complaint_attachments", "encrypted_at_utc", "`encrypted_at_utc` DATETIME(6) NULL"),
    ];

    private static readonly (string Table, string IndexName, string Definition)[] SchemaIndexesToEnsure =
    [
        ("complaint_attachments", "ix_attachments_sha256", "INDEX `ix_attachments_sha256` (`content_sha256`)"),
        ("complaints", "ix_complaints_key_date", "INDEX `ix_complaints_key_date` (`key_date`)"),
        ("complaints", "ix_complaints_flags", "INDEX `ix_complaints_flags` (`is_update`, `is_in_gestiona`, `is_rejected`)"),
        ("inbox_reports", "ix_inbox_reports_owner", "INDEX `ix_inbox_reports_owner` (`owner_user_id`)"),
        ("complaint_attachments", "ix_attachments_key_date", "INDEX `ix_attachments_key_date` (`key_date`)"),
    ];

    private static readonly (string Table, string Definition)[] SchemaEncryptedColumnsToEnsure =
    [
        ("complaints", "`gestiona_file_url` TEXT NOT NULL"),
        ("complaints", "`gestiona_file_code` TEXT NOT NULL"),
        ("complaints", "`tag` TEXT NOT NULL"),
        ("complaints", "`status_name` TEXT NOT NULL"),
        ("complaints", "`complaint_type` TEXT NOT NULL"),
        ("complaints", "`reporter_kind` TEXT NOT NULL"),
        ("complaints", "`reporter_first_name` TEXT NOT NULL"),
        ("complaints", "`reporter_first_surname` TEXT NOT NULL"),
        ("complaints", "`reporter_second_surname` TEXT NOT NULL"),
        ("complaints", "`reporter_last_name` TEXT NOT NULL"),
        ("complaints", "`reporter_business_name` TEXT NOT NULL"),
        ("complaints", "`reporter_gender` TEXT NOT NULL"),
        ("complaints", "`reporter_document_id` TEXT NOT NULL"),
        ("complaints", "`reporter_document_type` TEXT NOT NULL"),
        ("complaints", "`reporter_origin_country` TEXT NOT NULL"),
        ("complaints", "`subject` TEXT NOT NULL"),
        ("complaints", "`accused_party` TEXT NOT NULL"),
        ("complaints", "`accused_party_details` TEXT NOT NULL"),
        ("complaints", "`complaint_description` LONGTEXT NULL"),
        ("complaints", "`reported_to_institution` TEXT NOT NULL"),
        ("complaints", "`reported_institution_details` TEXT NOT NULL"),
        ("complaints", "`requested_protection` TEXT NOT NULL"),
        ("complaints", "`requested_protection_details` TEXT NOT NULL"),
        ("complaints", "`information_mode` TEXT NOT NULL"),
        ("complaints", "`facts_location` TEXT NOT NULL"),
        ("complaints", "`forwarding_authorization` TEXT NOT NULL"),
        ("complaints", "`forwarding_personal_data_preference` TEXT NOT NULL"),
        ("complaints", "`notification_preference` TEXT NOT NULL"),
        ("complaints", "`electronic_notification` TEXT NOT NULL"),
        ("complaints", "`online_tracking_preference` TEXT NOT NULL"),
        ("complaints", "`postal_notification_preference` TEXT NOT NULL"),
        ("complaints", "`email` TEXT NOT NULL"),
        ("complaints", "`sms_notification` TEXT NOT NULL"),
        ("complaints", "`comments` LONGTEXT NULL"),
        ("complaints", "`phone` TEXT NOT NULL"),
        ("complaints", "`address_line` TEXT NOT NULL"),
        ("complaints", "`address_road_type` TEXT NOT NULL"),
        ("complaints", "`address_number` TEXT NOT NULL"),
        ("complaints", "`address_floor` TEXT NOT NULL"),
        ("complaints", "`address_door` TEXT NOT NULL"),
        ("complaints", "`address_block` TEXT NOT NULL"),
        ("complaints", "`address_stair` TEXT NOT NULL"),
        ("complaints", "`address_extra` TEXT NOT NULL"),
        ("complaints", "`municipality` TEXT NOT NULL"),
        ("complaints", "`province` TEXT NOT NULL"),
        ("complaints", "`postal_code` TEXT NOT NULL"),
        ("complaints", "`country_code` TEXT NOT NULL"),
        ("complaints", "`form_fields_json` LONGTEXT NULL"),
        ("complaints", "`raw_report_text` LONGTEXT NULL"),
        ("complaints", "`display_name` TEXT NOT NULL"),
        ("complaints", "`workflow_status` TEXT NOT NULL"),
        ("complaints", "`selected_document_name` TEXT NULL"),
        ("complaints", "`gestiona_last_upload_type` TEXT NOT NULL"),
        ("complaints", "`gestiona_assigned_group` TEXT NOT NULL"),
        ("complaint_attachments", "`description` TEXT NULL"),
        ("complaint_attachments", "`notes` TEXT NOT NULL"),
    ];

    private readonly ComplaintStorageOptions _options;
    private readonly IHostEnvironment _environment;
    private readonly ILogger<MySqlDenunciaStore> _logger;
    private static readonly SemaphoreSlim SchemaGate = new(1, 1);
    private static volatile bool SchemaEnsured;
    private readonly MySqlConnectionStringProvider _connectionStringProvider;

    public MySqlDenunciaStore(
        IOptions<ComplaintStorageOptions> options,
        IHostEnvironment environment,
        ILogger<MySqlDenunciaStore> logger,
        MySqlConnectionStringProvider connectionStringProvider)
    {
        _options = options.Value;
        _environment = environment;
        _logger = logger;
        _connectionStringProvider = connectionStringProvider;
    }

    public Task EnsureSchemaAsync(CancellationToken cancellationToken = default)
    {
        return EnsureSchemaReadyAsync(cancellationToken);
    }

    public async Task<List<DenunciasGestiona>> GetAllDenunciasAsync(CancellationToken cancellationToken = default)
    {
        return await GetDenunciasAsync(null, DenunciaListScope.All, cancellationToken);
    }

    public async Task<List<DenunciasGestiona>> GetDenunciasByScopeAsync(
        DenunciaListScope scope,
        CancellationToken cancellationToken = default)
    {
        return await GetDenunciasAsync(null, scope, cancellationToken);
    }

    public async Task<List<DenunciasGestiona>> GetDenunciasByIdsAsync(
        IReadOnlyCollection<int> denunciaIds,
        CancellationToken cancellationToken = default)
    {
        var ids = NormalizeIds(denunciaIds);
        if (ids.Count == 0)
        {
            return [];
        }

        return await GetDenunciasAsync(ids, DenunciaListScope.All, cancellationToken);
    }

    public async Task<List<DenunciasGestiona>> GetDenunciasByIdsAsync(
        IReadOnlyCollection<int> denunciaIds,
        DenunciaListScope scope,
        CancellationToken cancellationToken = default)
    {
        var ids = NormalizeIds(denunciaIds);
        if (ids.Count == 0)
        {
            return [];
        }

        return await GetDenunciasAsync(ids, scope, cancellationToken);
    }

    private async Task<List<DenunciasGestiona>> GetDenunciasAsync(
        IReadOnlyList<int>? denunciaIds,
        DenunciaListScope scope,
        CancellationToken cancellationToken)
    {
        await EnsureSchemaReadyAsync(cancellationToken);

        await using var connection = await OpenConnectionAsync(cancellationToken);
        await using var command = connection.CreateCommand();
        var whereClauses = new List<string>();
        if (denunciaIds is { Count: > 0 })
        {
            var parameterNames = AddIntParameters(command, "id", denunciaIds);
            whereClauses.Add($"external_report_id IN ({string.Join(", ", parameterNames)})");
        }

        var scopeWhereClause = GetScopeWhereClause(scope);
        if (!string.IsNullOrWhiteSpace(scopeWhereClause))
        {
            whereClauses.Add(scopeWhereClause);
        }

        var whereSql = whereClauses.Count == 0
            ? string.Empty
            : $"WHERE {string.Join(" AND ", whereClauses)}";

        command.CommandText = $"""
            SELECT
            {ComplaintSelectColumns}
            FROM complaints
            {whereSql}
            ORDER BY COALESCE(gestiona_uploaded_at_utc, report_date_utc) DESC, external_report_id DESC;
            """;

        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        var result = new List<DenunciasGestiona>();
        while (await reader.ReadAsync(cancellationToken))
        {
            result.Add(MapComplaint(reader));
        }

        return result;
    }

    public async Task<List<FicherosDenuncias>> GetAllFicherosAsync(CancellationToken cancellationToken = default)
    {
        await EnsureSchemaReadyAsync(cancellationToken);

        var sql = $"""
            SELECT
            {AttachmentSelectColumns}
            FROM complaint_attachments a
            INNER JOIN complaints c ON c.id = a.complaint_id
            ORDER BY c.external_report_id DESC, a.original_file_name ASC;
            """;

        await using var connection = await OpenConnectionAsync(cancellationToken);
        await using var command = new MySqlCommand(sql, connection);

        var result = new List<FicherosDenuncias>();
        await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
        {
            while (await reader.ReadAsync(cancellationToken))
            {
                result.Add(MapAttachment(reader));
            }
        }

        await HydrateChunkedAttachmentsAsync(connection, result, cancellationToken);

        return result;
    }

    public async Task<List<FicherosDenuncias>> GetFicherosByDenunciaIdsAsync(
        IReadOnlyCollection<int> denunciaIds,
        CancellationToken cancellationToken = default)
    {
        await EnsureSchemaReadyAsync(cancellationToken);

        var ids = NormalizeIds(denunciaIds);
        if (ids.Count == 0)
        {
            return [];
        }

        await using var connection = await OpenConnectionAsync(cancellationToken);
        await using var command = connection.CreateCommand();
        var parameterNames = AddIntParameters(command, "id", ids);
        command.CommandText = $"""
            SELECT
            {AttachmentSelectColumns}
            FROM complaint_attachments a
            INNER JOIN complaints c ON c.id = a.complaint_id
            WHERE c.external_report_id IN ({string.Join(", ", parameterNames)})
            ORDER BY c.external_report_id DESC, a.original_file_name ASC;
            """;

        var result = new List<FicherosDenuncias>();
        await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
        {
            while (await reader.ReadAsync(cancellationToken))
            {
                result.Add(MapAttachment(reader));
            }
        }

        await HydrateChunkedAttachmentsAsync(connection, result, cancellationToken);

        return result;
    }

    public async Task<List<FicherosDenuncias>> GetFicherosByDenunciaAsync(
        int denunciaId,
        CancellationToken cancellationToken = default)
    {
        await EnsureSchemaReadyAsync(cancellationToken);

        var sql = $"""
            SELECT
            {AttachmentSelectColumns}
            FROM complaint_attachments a
            INNER JOIN complaints c ON c.id = a.complaint_id
            WHERE c.external_report_id = @denunciaId
            ORDER BY a.original_file_name ASC;
            """;

        await using var connection = await OpenConnectionAsync(cancellationToken);
        await using var command = new MySqlCommand(sql, connection);
        command.Parameters.AddWithValue("@denunciaId", denunciaId);

        var result = new List<FicherosDenuncias>();
        await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
        {
            while (await reader.ReadAsync(cancellationToken))
            {
                result.Add(MapAttachment(reader));
            }
        }

        await HydrateChunkedAttachmentsAsync(connection, result, cancellationToken);

        return result;
    }

    public async Task<List<FicherosDenuncias>> GetFicherosMetadataByDenunciaAsync(
        int denunciaId,
        CancellationToken cancellationToken = default)
    {
        await EnsureSchemaReadyAsync(cancellationToken);

        var sql = $"""
            SELECT
            {AttachmentMetadataSelectColumns}
            FROM complaint_attachments a
            INNER JOIN complaints c ON c.id = a.complaint_id
            WHERE c.external_report_id = @denunciaId
            ORDER BY a.original_file_name ASC;
            """;

        await using var connection = await OpenConnectionAsync(cancellationToken);
        await using var command = new MySqlCommand(sql, connection);
        command.Parameters.AddWithValue("@denunciaId", denunciaId);

        var result = new List<FicherosDenuncias>();
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        while (await reader.ReadAsync(cancellationToken))
        {
            result.Add(MapAttachment(reader));
        }

        return result;
    }

    public async Task<List<GestionaUploadHistoryEntry>> GetGestionaUploadHistoryAsync(CancellationToken cancellationToken = default)
    {
        await EnsureSchemaReadyAsync(cancellationToken);

        const string sql = """
            SELECT
                id,
                external_report_id,
                gestiona_file_url,
                gestiona_file_code,
                upload_type,
                assigned_group_code,
                uploaded_by_username,
                uploaded_at_utc,
                subject,
                document_names
            FROM gestiona_upload_history
            ORDER BY uploaded_at_utc DESC, id DESC;
            """;

        await using var connection = await OpenConnectionAsync(cancellationToken);
        await using var command = new MySqlCommand(sql, connection);

        var result = new List<GestionaUploadHistoryEntry>();
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        while (await reader.ReadAsync(cancellationToken))
        {
            result.Add(MapGestionaUploadHistory(reader));
        }

        return result;
    }

    public async Task<DenunciasGestiona?> GetDenunciaByIdAsync(
        int denunciaId,
        CancellationToken cancellationToken = default)
    {
        await EnsureSchemaReadyAsync(cancellationToken);

        var sql = $"""
            SELECT
            {ComplaintSelectColumns}
            FROM complaints
            WHERE external_report_id = @denunciaId
            LIMIT 1;
            """;

        await using var connection = await OpenConnectionAsync(cancellationToken);
        await using var command = new MySqlCommand(sql, connection);
        command.Parameters.AddWithValue("@denunciaId", denunciaId);

        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        return await reader.ReadAsync(cancellationToken)
            ? MapComplaint(reader)
            : null;
    }

    public async Task<DenunciasGestiona?> GetDenunciaByGestionaFileCodeAsync(
        string gestionaFileCode,
        CancellationToken cancellationToken = default)
    {
        await EnsureSchemaReadyAsync(cancellationToken);

        var sql = $"""
            SELECT
            {ComplaintSelectColumns}
            FROM complaints
            WHERE external_report_id = (
                SELECT history.external_report_id
                FROM gestiona_upload_history history
                WHERE history.gestiona_file_code = @gestionaFileCode
                ORDER BY history.uploaded_at_utc DESC, history.id DESC
                LIMIT 1
            )
               OR gestiona_file_code = @gestionaFileCode
            ORDER BY COALESCE(gestiona_uploaded_at_utc, report_date_utc) DESC, external_report_id DESC
            LIMIT 1;
            """;

        await using var connection = await OpenConnectionAsync(cancellationToken);
        await using var command = new MySqlCommand(sql, connection);
        command.Parameters.AddWithValue("@gestionaFileCode", gestionaFileCode.Trim());

        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        return await reader.ReadAsync(cancellationToken)
            ? MapComplaint(reader)
            : null;
    }

    public async Task AddGestionaUploadHistoryAsync(
        GestionaUploadHistoryCreateRequest request,
        string username,
        CancellationToken cancellationToken = default)
    {
        await EnsureSchemaReadyAsync(cancellationToken);

        const string sql = """
            INSERT INTO gestiona_upload_history (
                external_report_id,
                gestiona_file_url,
                gestiona_file_code,
                upload_type,
                assigned_group_code,
                uploaded_by_username,
                uploaded_at_utc,
                subject,
                document_names
            )
            SELECT
                @externalReportId,
                @gestionaFileUrl,
                @gestionaFileCode,
                @uploadType,
                @assignedGroupCode,
                @uploadedByUsername,
                @uploadedAtUtc,
                @subject,
                @documentNames
            WHERE NOT EXISTS (
                SELECT 1
                FROM gestiona_upload_history
                WHERE external_report_id = @externalReportId
                  AND uploaded_at_utc = @uploadedAtUtc
                  AND upload_type = @uploadType
                LIMIT 1
            );
            """;

        await using var connection = await OpenConnectionAsync(cancellationToken);
        await using var command = new MySqlCommand(sql, connection);
        command.Parameters.AddWithValue("@externalReportId", request.DenunciaId);
        command.Parameters.AddWithValue("@gestionaFileUrl", request.ExpedienteGestionaUrl ?? string.Empty);
        command.Parameters.AddWithValue("@gestionaFileCode", request.CodigoExpedienteGestiona ?? string.Empty);
        command.Parameters.AddWithValue("@uploadType", request.TipoSubida ?? string.Empty);
        command.Parameters.AddWithValue("@assignedGroupCode", ExtractGroupCode(request.GrupoAsignado));
        command.Parameters.AddWithValue("@uploadedByUsername", string.IsNullOrWhiteSpace(username) ? "(sin usuario)" : username.Trim());
        command.Parameters.AddWithValue("@uploadedAtUtc", request.UploadedAtUtc == DateTime.MinValue ? DateTime.UtcNow : request.UploadedAtUtc);
        command.Parameters.AddWithValue("@subject", ToDbStringOrNull(request.Asunto));
        command.Parameters.AddWithValue("@documentNames", ToDbStringOrNull(request.Documentos));

        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    public async Task UpsertDenunciaAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken = default)
    {
        await EnsureSchemaReadyAsync(cancellationToken);

        const string sql = """
            INSERT INTO complaints (
                external_registry_id,
                external_report_id,
                report_date_utc,
                gestiona_file_url,
                gestiona_file_code,
                gestiona_person_id,
                tag,
                status_name,
                complaint_type,
                reporter_kind,
                is_legal_entity,
                reporter_first_name,
                reporter_first_surname,
                reporter_second_surname,
                reporter_last_name,
                reporter_business_name,
                reporter_gender,
                reporter_document_id,
                reporter_document_type,
                reporter_origin_country,
                subject,
                accused_party,
                accused_party_details,
                complaint_description,
                reported_to_institution,
                reported_institution_details,
                requested_protection,
                requested_protection_details,
                information_mode,
                facts_location,
                facts_date_utc,
                forwarding_authorization,
                forwarding_personal_data_preference,
                notification_preference,
                electronic_notification,
                online_tracking_preference,
                postal_notification_preference,
                email,
                sms_notification,
                accepted_terms,
                comments,
                phone,
                address_line,
                address_road_type,
                address_number,
                address_floor,
                address_door,
                address_block,
                address_stair,
                address_extra,
                municipality,
                province,
                postal_code,
                country_code,
                form_fields_json,
                raw_report_text,
                is_confidential,
                is_update,
                gestiona_procedure_id,
                gestiona_group_id,
                display_name,
                workflow_status,
                selected_document_name,
                gestiona_uploaded_at_utc,
                gestiona_last_upload_type,
                gestiona_assigned_group,
                pending_update_source,
                is_in_gestiona,
                is_rejected,
                key_date,
                encryption_scheme,
                encrypted_at_utc
            ) VALUES (
                @externalRegistryId,
                @externalReportId,
                @reportDateUtc,
                @gestionaFileUrl,
                @gestionaFileCode,
                @gestionaPersonId,
                @tag,
                @statusName,
                @complaintType,
                @reporterKind,
                @isLegalEntity,
                @reporterFirstName,
                @reporterFirstSurname,
                @reporterSecondSurname,
                @reporterLastName,
                @reporterBusinessName,
                @reporterGender,
                @reporterDocumentId,
                @reporterDocumentType,
                @reporterOriginCountry,
                @subject,
                @accusedParty,
                @accusedPartyDetails,
                @complaintDescription,
                @reportedToInstitution,
                @reportedInstitutionDetails,
                @requestedProtection,
                @requestedProtectionDetails,
                @informationMode,
                @factsLocation,
                @factsDateUtc,
                @forwardingAuthorization,
                @forwardingPersonalDataPreference,
                @notificationPreference,
                @electronicNotification,
                @onlineTrackingPreference,
                @postalNotificationPreference,
                @email,
                @smsNotification,
                @acceptedTerms,
                @comments,
                @phone,
                @addressLine,
                @addressRoadType,
                @addressNumber,
                @addressFloor,
                @addressDoor,
                @addressBlock,
                @addressStair,
                @addressExtra,
                @municipality,
                @province,
                @postalCode,
                @countryCode,
                @formFieldsJson,
                @rawReportText,
                @isConfidential,
                @isUpdate,
                @gestionaProcedureId,
                @gestionaGroupId,
                @displayName,
                @workflowStatus,
                @selectedDocumentName,
                @gestionaUploadedAtUtc,
                @gestionaLastUploadType,
                @gestionaAssignedGroup,
                @pendingUpdateSource,
                @isInGestiona,
                @isRejected,
                @keyDate,
                @encryptionScheme,
                @encryptedAtUtc
            )
            ON DUPLICATE KEY UPDATE
                external_registry_id = VALUES(external_registry_id),
                report_date_utc = VALUES(report_date_utc),
                gestiona_file_url = VALUES(gestiona_file_url),
                gestiona_file_code = VALUES(gestiona_file_code),
                gestiona_person_id = VALUES(gestiona_person_id),
                tag = VALUES(tag),
                status_name = VALUES(status_name),
                complaint_type = VALUES(complaint_type),
                reporter_kind = VALUES(reporter_kind),
                is_legal_entity = VALUES(is_legal_entity),
                reporter_first_name = VALUES(reporter_first_name),
                reporter_first_surname = VALUES(reporter_first_surname),
                reporter_second_surname = VALUES(reporter_second_surname),
                reporter_last_name = VALUES(reporter_last_name),
                reporter_business_name = VALUES(reporter_business_name),
                reporter_gender = VALUES(reporter_gender),
                reporter_document_id = VALUES(reporter_document_id),
                reporter_document_type = VALUES(reporter_document_type),
                reporter_origin_country = VALUES(reporter_origin_country),
                subject = VALUES(subject),
                accused_party = VALUES(accused_party),
                accused_party_details = VALUES(accused_party_details),
                complaint_description = VALUES(complaint_description),
                reported_to_institution = VALUES(reported_to_institution),
                reported_institution_details = VALUES(reported_institution_details),
                requested_protection = VALUES(requested_protection),
                requested_protection_details = VALUES(requested_protection_details),
                information_mode = VALUES(information_mode),
                facts_location = VALUES(facts_location),
                facts_date_utc = VALUES(facts_date_utc),
                forwarding_authorization = VALUES(forwarding_authorization),
                forwarding_personal_data_preference = VALUES(forwarding_personal_data_preference),
                notification_preference = VALUES(notification_preference),
                electronic_notification = VALUES(electronic_notification),
                online_tracking_preference = VALUES(online_tracking_preference),
                postal_notification_preference = VALUES(postal_notification_preference),
                email = VALUES(email),
                sms_notification = VALUES(sms_notification),
                accepted_terms = VALUES(accepted_terms),
                comments = VALUES(comments),
                phone = VALUES(phone),
                address_line = VALUES(address_line),
                address_road_type = VALUES(address_road_type),
                address_number = VALUES(address_number),
                address_floor = VALUES(address_floor),
                address_door = VALUES(address_door),
                address_block = VALUES(address_block),
                address_stair = VALUES(address_stair),
                address_extra = VALUES(address_extra),
                municipality = VALUES(municipality),
                province = VALUES(province),
                postal_code = VALUES(postal_code),
                country_code = VALUES(country_code),
                form_fields_json = VALUES(form_fields_json),
                raw_report_text = VALUES(raw_report_text),
                is_confidential = VALUES(is_confidential),
                is_update = VALUES(is_update),
                gestiona_procedure_id = VALUES(gestiona_procedure_id),
                gestiona_group_id = VALUES(gestiona_group_id),
                display_name = VALUES(display_name),
                workflow_status = VALUES(workflow_status),
                selected_document_name = VALUES(selected_document_name),
                gestiona_uploaded_at_utc = VALUES(gestiona_uploaded_at_utc),
                gestiona_last_upload_type = VALUES(gestiona_last_upload_type),
                gestiona_assigned_group = VALUES(gestiona_assigned_group),
                pending_update_source = VALUES(pending_update_source),
                is_in_gestiona = VALUES(is_in_gestiona),
                is_rejected = VALUES(is_rejected),
                key_date = VALUES(key_date),
                encryption_scheme = VALUES(encryption_scheme),
                encrypted_at_utc = VALUES(encrypted_at_utc),
                updated_at_utc = UTC_TIMESTAMP(6);
            """;

        await using var connection = await OpenConnectionAsync(cancellationToken);
        await using var command = new MySqlCommand(sql, connection);

        command.Parameters.AddWithValue("@externalRegistryId", denuncia.Id_RegistroDenuncia);
        command.Parameters.AddWithValue("@externalReportId", denuncia.Id_Denuncia);
        command.Parameters.AddWithValue("@reportDateUtc", ToDbDate(denuncia.Fecha));
        command.Parameters.AddWithValue("@gestionaFileUrl", denuncia.Expediente_Gestiona ?? string.Empty);
        command.Parameters.AddWithValue("@gestionaFileCode", denuncia.CodigoExpedienteGestiona ?? string.Empty);
        command.Parameters.AddWithValue("@gestionaPersonId", denuncia.Id_Persona_Gestiona);
        command.Parameters.AddWithValue("@tag", denuncia.Etiqueta ?? string.Empty);
        command.Parameters.AddWithValue("@statusName", denuncia.Estado ?? string.Empty);
        command.Parameters.AddWithValue("@complaintType", denuncia.Tipo_Denuncia ?? string.Empty);
        command.Parameters.AddWithValue("@reporterKind", denuncia.TipoDenunciante ?? string.Empty);
        command.Parameters.AddWithValue("@isLegalEntity", denuncia.EsPersonaJuridica);
        command.Parameters.AddWithValue("@reporterFirstName", denuncia.Nombre ?? string.Empty);
        command.Parameters.AddWithValue("@reporterFirstSurname", denuncia.PrimerApellido ?? string.Empty);
        command.Parameters.AddWithValue("@reporterSecondSurname", denuncia.SegundoApellido ?? string.Empty);
        command.Parameters.AddWithValue("@reporterLastName", denuncia.Apellidos ?? string.Empty);
        command.Parameters.AddWithValue("@reporterBusinessName", denuncia.RazonSocial ?? string.Empty);
        command.Parameters.AddWithValue("@reporterGender", denuncia.Sexo ?? string.Empty);
        command.Parameters.AddWithValue("@reporterDocumentId", denuncia.Dni ?? string.Empty);
        command.Parameters.AddWithValue("@reporterDocumentType", denuncia.TipoDocumentoIdentificativo ?? string.Empty);
        command.Parameters.AddWithValue("@reporterOriginCountry", denuncia.PaisOrigen ?? string.Empty);
        command.Parameters.AddWithValue("@subject", denuncia.Asunto ?? string.Empty);
        command.Parameters.AddWithValue("@accusedParty", denuncia.A_Quien_Denuncia ?? string.Empty);
        command.Parameters.AddWithValue("@accusedPartyDetails", denuncia.DenunciadoDetalle ?? string.Empty);
        command.Parameters.AddWithValue("@complaintDescription", ToDbStringOrNull(denuncia.Descripcion_Denuncia));
        command.Parameters.AddWithValue("@reportedToInstitution", denuncia.Denunciado_Ante_Inst ?? string.Empty);
        command.Parameters.AddWithValue("@reportedInstitutionDetails", denuncia.OrganismoDenunciado ?? string.Empty);
        command.Parameters.AddWithValue("@requestedProtection", denuncia.SolicitaProteccion ?? string.Empty);
        command.Parameters.AddWithValue("@requestedProtectionDetails", denuncia.MedidasProteccionSolicitadas ?? string.Empty);
        command.Parameters.AddWithValue("@informationMode", denuncia.Modalidad_Informacion ?? string.Empty);
        command.Parameters.AddWithValue("@factsLocation", denuncia.Lugar_Hechos ?? string.Empty);
        command.Parameters.AddWithValue("@factsDateUtc", ToDbDate(denuncia.Fecha_Hechos));
        command.Parameters.AddWithValue("@forwardingAuthorization", denuncia.AutorizaRemision ?? string.Empty);
        command.Parameters.AddWithValue("@forwardingPersonalDataPreference", denuncia.PreferenciaRemision ?? string.Empty);
        command.Parameters.AddWithValue("@notificationPreference", denuncia.Notificacion_Preferencia ?? string.Empty);
        command.Parameters.AddWithValue("@electronicNotification", denuncia.Notificacion_Electronica ?? string.Empty);
        command.Parameters.AddWithValue("@onlineTrackingPreference", denuncia.SeguimientoOnline ?? string.Empty);
        command.Parameters.AddWithValue("@postalNotificationPreference", denuncia.NotificacionPostal ?? string.Empty);
        command.Parameters.AddWithValue("@email", denuncia.Correo_Electronico ?? string.Empty);
        command.Parameters.AddWithValue("@smsNotification", denuncia.Notificacion_Sms ?? string.Empty);
        command.Parameters.AddWithValue("@acceptedTerms", denuncia.Condiciones);
        command.Parameters.AddWithValue("@comments", ToDbStringOrNull(denuncia.Comments));
        command.Parameters.AddWithValue("@phone", denuncia.Telefono ?? string.Empty);
        command.Parameters.AddWithValue("@addressLine", denuncia.Direccion ?? string.Empty);
        command.Parameters.AddWithValue("@addressRoadType", denuncia.DireccionTipoVia ?? string.Empty);
        command.Parameters.AddWithValue("@addressNumber", denuncia.DireccionNumero ?? string.Empty);
        command.Parameters.AddWithValue("@addressFloor", denuncia.DireccionPiso ?? string.Empty);
        command.Parameters.AddWithValue("@addressDoor", denuncia.DireccionPuerta ?? string.Empty);
        command.Parameters.AddWithValue("@addressBlock", denuncia.DireccionBloque ?? string.Empty);
        command.Parameters.AddWithValue("@addressStair", denuncia.DireccionEscalera ?? string.Empty);
        command.Parameters.AddWithValue("@addressExtra", denuncia.DireccionExtra ?? string.Empty);
        command.Parameters.AddWithValue("@municipality", denuncia.Municipio ?? string.Empty);
        command.Parameters.AddWithValue("@province", denuncia.Provincia ?? string.Empty);
        command.Parameters.AddWithValue("@postalCode", denuncia.CodigoPostal ?? string.Empty);
        command.Parameters.AddWithValue("@countryCode", denuncia.Pais ?? string.Empty);
        command.Parameters.AddWithValue("@formFieldsJson", ToDbStringOrNull(denuncia.CamposFormularioJson));
        command.Parameters.AddWithValue("@rawReportText", ToDbStringOrNull(denuncia.TextoOriginalReport));
        command.Parameters.AddWithValue("@isConfidential", denuncia.Confidencial);
        command.Parameters.AddWithValue("@isUpdate", denuncia.EsActualizacion);
        command.Parameters.AddWithValue("@gestionaProcedureId", ToDbGuid(denuncia.ProcedureId));
        command.Parameters.AddWithValue("@gestionaGroupId", ToDbGuid(denuncia.GroupId));
        command.Parameters.AddWithValue("@displayName", denuncia.NombreDenuncia ?? string.Empty);
        command.Parameters.AddWithValue("@workflowStatus", denuncia.EstadoDenuncia ?? string.Empty);
        command.Parameters.AddWithValue("@selectedDocumentName", ToDbStringOrNull(denuncia.ArchivoElegido));
        command.Parameters.AddWithValue("@gestionaUploadedAtUtc", ToDbDate(denuncia.FechaSubidaAGestiona));
        command.Parameters.AddWithValue("@gestionaLastUploadType", denuncia.UltimaSubidaGestionaTipo ?? string.Empty);
        command.Parameters.AddWithValue("@gestionaAssignedGroup", denuncia.UltimoGrupoAsignadoGestiona ?? string.Empty);
        command.Parameters.AddWithValue("@pendingUpdateSource", denuncia.PendingUpdateSource ?? string.Empty);
        command.Parameters.AddWithValue("@isInGestiona", denuncia.EnGestiona);
        command.Parameters.AddWithValue("@isRejected", denuncia.EnRechazada);
        command.Parameters.AddWithValue("@keyDate", ToDbDate(denuncia.KeyDate));
        command.Parameters.AddWithValue("@encryptionScheme", denuncia.EncryptionScheme ?? string.Empty);
        command.Parameters.AddWithValue("@encryptedAtUtc", ToDbDate(denuncia.EncryptedAtUtc));

        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    public async Task UpsertFicherosAsync(
        IEnumerable<FicherosDenuncias> ficheros,
        CancellationToken cancellationToken = default)
    {
        await EnsureSchemaReadyAsync(cancellationToken);

        var attachments = ficheros.ToList();
        if (attachments.Count == 0)
        {
            return;
        }

        const string sql = """
            INSERT INTO complaint_attachments (
                complaint_id,
                attachment_type_id,
                description,
                attachment_date_utc,
                notes,
                original_file_name,
                content,
                content_mime_type,
                content_sha256,
                uploaded_to_gestiona,
                uploaded_at_utc,
                key_date,
                encryption_scheme,
                encrypted_at_utc
            ) VALUES (
                (
                    SELECT c.id
                    FROM complaints c
                    WHERE c.external_report_id = @externalReportId
                    LIMIT 1
                ),
                @attachmentTypeId,
                @description,
                @attachmentDateUtc,
                @notes,
                @originalFileName,
                @content,
                @contentMimeType,
                @contentSha256,
                @uploadedToGestiona,
                @uploadedAtUtc,
                @keyDate,
                @encryptionScheme,
                @encryptedAtUtc
            )
            ON DUPLICATE KEY UPDATE
                id = LAST_INSERT_ID(id),
                attachment_type_id = @attachmentTypeId,
                description = @description,
                attachment_date_utc = @attachmentDateUtc,
                notes = @notes,
                uploaded_to_gestiona = CASE
                    WHEN complaint_attachments.content_sha256 = @contentSha256 THEN complaint_attachments.uploaded_to_gestiona
                    ELSE @uploadedToGestiona
                END,
                uploaded_at_utc = CASE
                    WHEN complaint_attachments.content_sha256 = @contentSha256 THEN complaint_attachments.uploaded_at_utc
                    ELSE @uploadedAtUtc
                END,
                content = @content,
                content_mime_type = @contentMimeType,
                content_sha256 = @contentSha256,
                key_date = @keyDate,
                encryption_scheme = @encryptionScheme,
                encrypted_at_utc = @encryptedAtUtc,
                updated_at_utc = CURRENT_TIMESTAMP(6);
            """;

        await using var connection = await OpenConnectionAsync(cancellationToken);
        await using var transaction = await connection.BeginTransactionAsync(cancellationToken);

        try
        {
            foreach (var fichero in attachments)
            {
                await using var command = new MySqlCommand(sql, connection, (MySqlTransaction)transaction);
                var content = fichero.Fichero ?? [];
                var useChunks = content.Length > AttachmentChunkThresholdBytes;
                var contentToStore = useChunks
                    ? CreateChunkedAttachmentMarker(content.LongLength, GetChunkCount(content.Length))
                    : content;
                var sha256 = string.IsNullOrWhiteSpace(fichero.ContentSha256)
                    ? ComputeSha256Hex(content)
                    : fichero.ContentSha256.Trim().ToLowerInvariant();

                command.Parameters.AddWithValue("@attachmentTypeId", fichero.Id_Tipo);
                command.Parameters.AddWithValue("@description", ToDbStringOrNull(fichero.Descripcion));
                command.Parameters.AddWithValue("@attachmentDateUtc", ToDbDate(fichero.Fecha));
                command.Parameters.AddWithValue("@notes", fichero.Observaciones ?? string.Empty);
                command.Parameters.AddWithValue("@originalFileName", fichero.NombreFichero ?? string.Empty);
                command.Parameters.Add("@content", MySqlDbType.LongBlob).Value = contentToStore;
                command.Parameters.AddWithValue("@contentMimeType", DetectMimeType(fichero.NombreFichero));
                command.Parameters.AddWithValue("@contentSha256", sha256);
                command.Parameters.AddWithValue("@uploadedToGestiona", fichero.Subido);
                command.Parameters.AddWithValue("@uploadedAtUtc", ToDbDate(fichero.FechaSubida));
                command.Parameters.AddWithValue("@keyDate", ToDbDate(fichero.KeyDate));
                command.Parameters.AddWithValue("@encryptionScheme", fichero.EncryptionScheme ?? string.Empty);
                command.Parameters.AddWithValue("@encryptedAtUtc", ToDbDate(fichero.EncryptedAtUtc));
                command.Parameters.AddWithValue("@externalReportId", fichero.Id_Denuncia);

                await command.ExecuteNonQueryAsync(cancellationToken);

                var attachmentId = command.LastInsertedId;
                if (attachmentId <= 0)
                {
                    attachmentId = await GetAttachmentIdAsync(
                        connection,
                        (MySqlTransaction)transaction,
                        fichero.Id_Denuncia,
                        fichero.NombreFichero ?? string.Empty,
                        cancellationToken);
                }

                if (useChunks)
                {
                    await ReplaceAttachmentChunksAsync(
                        connection,
                        (MySqlTransaction)transaction,
                        attachmentId,
                        content,
                        cancellationToken);
                }
                else
                {
                    await DeleteAttachmentChunksAsync(
                        connection,
                        (MySqlTransaction)transaction,
                        attachmentId,
                        cancellationToken);
                }
            }

            await transaction.CommitAsync(cancellationToken);
        }
        catch
        {
            try
            {
                await transaction.RollbackAsync(cancellationToken);
            }
            catch (Exception rollbackException)
            {
                _logger.LogWarning(
                    rollbackException,
                    "No se ha podido deshacer la transaccion de adjuntos porque la conexion ya no estaba disponible.");
            }

            throw;
        }
    }

    private static async Task<long> GetAttachmentIdAsync(
        MySqlConnection connection,
        MySqlTransaction transaction,
        int externalReportId,
        string originalFileName,
        CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT a.id
            FROM complaint_attachments a
            INNER JOIN complaints c ON c.id = a.complaint_id
            WHERE c.external_report_id = @externalReportId
              AND a.original_file_name = @originalFileName
            LIMIT 1;
            """;

        await using var command = new MySqlCommand(sql, connection, transaction);
        command.Parameters.AddWithValue("@externalReportId", externalReportId);
        command.Parameters.AddWithValue("@originalFileName", originalFileName);

        var result = await command.ExecuteScalarAsync(cancellationToken);
        if (result is null || result is DBNull)
        {
            throw new InvalidOperationException(
                $"No se ha encontrado el adjunto '{originalFileName}' de la denuncia #{externalReportId} tras guardarlo.");
        }

        return Convert.ToInt64(result, CultureInfo.InvariantCulture);
    }

    private static async Task ReplaceAttachmentChunksAsync(
        MySqlConnection connection,
        MySqlTransaction transaction,
        long attachmentId,
        byte[] content,
        CancellationToken cancellationToken)
    {
        await DeleteAttachmentChunksAsync(connection, transaction, attachmentId, cancellationToken);

        const string sql = """
            INSERT INTO complaint_attachment_chunks (
                attachment_id,
                chunk_index,
                content
            ) VALUES (
                @attachmentId,
                @chunkIndex,
                @content
            );
            """;

        var chunkIndex = 0;
        for (var offset = 0; offset < content.Length; offset += AttachmentChunkSizeBytes)
        {
            var length = Math.Min(AttachmentChunkSizeBytes, content.Length - offset);
            var chunk = new byte[length];
            Buffer.BlockCopy(content, offset, chunk, 0, length);

            await using var command = new MySqlCommand(sql, connection, transaction);
            command.Parameters.AddWithValue("@attachmentId", attachmentId);
            command.Parameters.AddWithValue("@chunkIndex", chunkIndex);
            command.Parameters.Add("@content", MySqlDbType.LongBlob).Value = chunk;

            await command.ExecuteNonQueryAsync(cancellationToken);
            chunkIndex++;
        }
    }

    private static async Task DeleteAttachmentChunksAsync(
        MySqlConnection connection,
        MySqlTransaction transaction,
        long attachmentId,
        CancellationToken cancellationToken)
    {
        const string sql = """
            DELETE FROM complaint_attachment_chunks
            WHERE attachment_id = @attachmentId;
            """;

        await using var command = new MySqlCommand(sql, connection, transaction);
        command.Parameters.AddWithValue("@attachmentId", attachmentId);
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    public async Task MarkFicherosAsUploadedAsync(
        int denunciaId,
        IEnumerable<string> fileNames,
        DateTime uploadedAtUtc,
        CancellationToken cancellationToken = default)
    {
        await EnsureSchemaReadyAsync(cancellationToken);

        var names = fileNames
            .Where(name => !string.IsNullOrWhiteSpace(name))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToList();

        if (names.Count == 0)
        {
            return;
        }

        var parameterNames = new List<string>(names.Count);
        await using var connection = await OpenConnectionAsync(cancellationToken);
        await using var command = connection.CreateCommand();

        for (var i = 0; i < names.Count; i++)
        {
            var parameterName = $"@name{i}";
            parameterNames.Add(parameterName);
            command.Parameters.AddWithValue(parameterName, names[i]);
        }

        command.Parameters.AddWithValue("@denunciaId", denunciaId);
        command.Parameters.AddWithValue("@uploadedAtUtc", uploadedAtUtc);
        command.CommandText = $"""
            UPDATE complaint_attachments a
            INNER JOIN complaints c ON c.id = a.complaint_id
            SET
                a.uploaded_to_gestiona = 1,
                a.uploaded_at_utc = @uploadedAtUtc,
                a.updated_at_utc = UTC_TIMESTAMP(6)
            WHERE c.external_report_id = @denunciaId
              AND a.original_file_name IN ({string.Join(", ", parameterNames)});
            """;

        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    private static List<int> NormalizeIds(IEnumerable<int> denunciaIds)
    {
        return denunciaIds
            .Where(id => id > 0)
            .Distinct()
            .ToList();
    }

    private static string GetScopeWhereClause(DenunciaListScope scope)
    {
        return scope switch
        {
            DenunciaListScope.Pending => "is_in_gestiona = 0 AND is_rejected = 0 AND is_update = 0",
            DenunciaListScope.Updates => "is_update = 1",
            DenunciaListScope.InGestiona => "is_in_gestiona = 1",
            DenunciaListScope.Rejected => "is_rejected = 1",
            _ => string.Empty
        };
    }

    private static List<string> AddIntParameters(MySqlCommand command, string prefix, IReadOnlyList<int> ids)
    {
        var parameterNames = new List<string>(ids.Count);
        for (var i = 0; i < ids.Count; i++)
        {
            var parameterName = $"@{prefix}{i}";
            command.Parameters.AddWithValue(parameterName, ids[i]);
            parameterNames.Add(parameterName);
        }

        return parameterNames;
    }

    private async Task EnsureSchemaReadyAsync(CancellationToken cancellationToken)
    {
        if (SchemaEnsured)
        {
            return;
        }

        await SchemaGate.WaitAsync(cancellationToken);
        try
        {
            if (SchemaEnsured)
            {
                return;
            }

            if (_options.AutoCreateSchema)
            {
                var sqlPath = ResolveSchemaPath();
                if (!File.Exists(sqlPath))
                {
                    throw new FileNotFoundException($"No se encuentra el script de esquema: {sqlPath}");
                }

                var sql = await File.ReadAllTextAsync(sqlPath, cancellationToken);
                var commands = sql
                    .Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
                    .Where(command => !string.IsNullOrWhiteSpace(command))
                    .ToList();

                await using var connection = await OpenConnectionAsync(cancellationToken);
                foreach (var commandText in commands)
                {
                    await using var command = new MySqlCommand(commandText, connection);
                    await command.ExecuteNonQueryAsync(cancellationToken);
                }

                await EnsureCompatibleSchemaAsync(connection, cancellationToken);

                _logger.LogInformation("Esquema de denuncias verificado/creado en MySQL.");
            }

            SchemaEnsured = true;
        }
        finally
        {
            SchemaGate.Release();
        }
    }

    private async Task<MySqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
    {
        var connectionString = await _connectionStringProvider.GetConnectionStringAsync(cancellationToken);
        var connection = new MySqlConnection(connectionString);
        await connection.OpenAsync(cancellationToken);

        await using var timeZoneCommand = new MySqlCommand("SET time_zone = '+00:00';", connection);
        await timeZoneCommand.ExecuteNonQueryAsync(cancellationToken);

        return connection;
    }

    private static async Task EnsureCompatibleSchemaAsync(
        MySqlConnection connection,
        CancellationToken cancellationToken)
    {
        await EnsureAttachmentChunksTableAsync(connection, cancellationToken);
        await EnsureGestionaUploadHistoryTableAsync(connection, cancellationToken);

        foreach (var (table, column, definition) in SchemaColumnsToEnsure)
        {
            if (!await ColumnExistsAsync(connection, table, column, cancellationToken))
            {
                await using var alterCommand = connection.CreateCommand();
                alterCommand.CommandText = $"ALTER TABLE `{table}` ADD COLUMN {definition};";
                await alterCommand.ExecuteNonQueryAsync(cancellationToken);
            }
        }

        foreach (var (table, indexName, definition) in SchemaIndexesToEnsure)
        {
            if (!await IndexExistsAsync(connection, table, indexName, cancellationToken))
            {
                await using var alterCommand = connection.CreateCommand();
                alterCommand.CommandText = $"ALTER TABLE `{table}` ADD {definition};";
                await alterCommand.ExecuteNonQueryAsync(cancellationToken);
            }
        }

        foreach (var (table, definition) in SchemaEncryptedColumnsToEnsure)
        {
            var column = ExtractColumnName(definition);
            if (!await ColumnDataTypeIsCompatibleAsync(connection, table, column, definition, cancellationToken))
            {
                await using var alterCommand = connection.CreateCommand();
                alterCommand.CommandText = $"ALTER TABLE `{table}` MODIFY COLUMN {definition};";
                await alterCommand.ExecuteNonQueryAsync(cancellationToken);
            }
        }

        await BackfillReportOwnersAsync(connection, cancellationToken);
    }

    private static async Task BackfillReportOwnersAsync(
        MySqlConnection connection,
        CancellationToken cancellationToken)
    {
        const string sql = """
            UPDATE inbox_reports ir
            LEFT JOIN (
                SELECT
                    uir.inbox_report_id,
                    CAST(
                        SUBSTRING_INDEX(
                            GROUP_CONCAT(
                                uir.app_user_id
                                ORDER BY
                                    COALESCE(
                                        uir.first_downloaded_at_utc,
                                        uir.last_downloaded_at_utc,
                                        uir.first_seen_at_utc
                                    ),
                                    uir.app_user_id
                                SEPARATOR ','
                            ),
                            ',',
                            1
                        ) AS UNSIGNED
                    ) AS first_owner_user_id
                FROM user_inbox_reports uir
                WHERE uir.first_downloaded_at_utc IS NOT NULL
                   OR uir.last_downloaded_at_utc IS NOT NULL
                GROUP BY uir.inbox_report_id
            ) first_owner ON first_owner.inbox_report_id = ir.id
            SET ir.owner_user_id = COALESCE(
                first_owner.first_owner_user_id,
                ir.last_downloaded_by_user_id
            )
            WHERE ir.owner_user_id IS NULL
              AND (
                    ir.imported_to_store_at_utc IS NOT NULL
                 OR ir.imported_complaint_report_id IS NOT NULL
              );
            """;

        await using var command = new MySqlCommand(sql, connection);
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    private static async Task EnsureAttachmentChunksTableAsync(
        MySqlConnection connection,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.CommandText = AttachmentChunksTableSql;
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    private static async Task EnsureGestionaUploadHistoryTableAsync(
        MySqlConnection connection,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.CommandText = GestionaUploadHistoryTableSql;
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    private static string ExtractColumnName(string definition)
    {
        var first = definition.IndexOf('`');
        var second = first >= 0 ? definition.IndexOf('`', first + 1) : -1;
        return first >= 0 && second > first
            ? definition[(first + 1)..second]
            : definition.Split(' ', StringSplitOptions.RemoveEmptyEntries)[0].Trim('`');
    }

    private static async Task<bool> ColumnDataTypeIsCompatibleAsync(
        MySqlConnection connection,
        string table,
        string column,
        string definition,
        CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT DATA_TYPE
            FROM information_schema.COLUMNS
            WHERE TABLE_SCHEMA = DATABASE()
              AND TABLE_NAME = @table
              AND COLUMN_NAME = @column
            LIMIT 1;
            """;

        await using var command = connection.CreateCommand();
        command.CommandText = sql;
        command.Parameters.AddWithValue("@table", table);
        command.Parameters.AddWithValue("@column", column);

        var current = (await command.ExecuteScalarAsync(cancellationToken))?.ToString();
        if (string.IsNullOrWhiteSpace(current))
        {
            return false;
        }

        var expected = definition.ToUpperInvariant();
        if (expected.Contains("LONGTEXT", StringComparison.Ordinal))
        {
            return string.Equals(current, "longtext", StringComparison.OrdinalIgnoreCase);
        }

        if (expected.Contains("TEXT", StringComparison.Ordinal))
        {
            return current.Equals("text", StringComparison.OrdinalIgnoreCase) ||
                   current.Equals("mediumtext", StringComparison.OrdinalIgnoreCase) ||
                   current.Equals("longtext", StringComparison.OrdinalIgnoreCase);
        }

        return true;
    }

    private static async Task<bool> ColumnExistsAsync(
        MySqlConnection connection,
        string table,
        string column,
        CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT COUNT(*)
            FROM information_schema.COLUMNS
            WHERE TABLE_SCHEMA = DATABASE()
              AND TABLE_NAME = @table
              AND COLUMN_NAME = @column;
            """;

        await using var command = connection.CreateCommand();
        command.CommandText = sql;
        command.Parameters.AddWithValue("@table", table);
        command.Parameters.AddWithValue("@column", column);

        var result = await command.ExecuteScalarAsync(cancellationToken);
        return Convert.ToInt32(result, CultureInfo.InvariantCulture) > 0;
    }

    private static async Task<bool> IndexExistsAsync(
        MySqlConnection connection,
        string table,
        string indexName,
        CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT COUNT(*)
            FROM information_schema.STATISTICS
            WHERE TABLE_SCHEMA = DATABASE()
              AND TABLE_NAME = @table
              AND INDEX_NAME = @indexName;
            """;

        await using var command = connection.CreateCommand();
        command.CommandText = sql;
        command.Parameters.AddWithValue("@table", table);
        command.Parameters.AddWithValue("@indexName", indexName);

        var result = await command.ExecuteScalarAsync(cancellationToken);
        return Convert.ToInt32(result, CultureInfo.InvariantCulture) > 0;
    }

    private string ResolveSchemaPath()
    {
        var contentRootPath = Path.Combine(_environment.ContentRootPath, "Scripts", "gestiondenuncias_schema.sql");
        if (File.Exists(contentRootPath))
        {
            return contentRootPath;
        }

        return Path.Combine(AppContext.BaseDirectory, "Scripts", "gestiondenuncias_schema.sql");
    }

    private static async Task HydrateChunkedAttachmentsAsync(
        MySqlConnection connection,
        IEnumerable<FicherosDenuncias> attachments,
        CancellationToken cancellationToken)
    {
        foreach (var attachment in attachments)
        {
            var content = attachment.Fichero;
            if (content is null ||
                !TryParseChunkedAttachmentMarker(content, out var totalLength, out var chunkCount))
            {
                continue;
            }

            attachment.Fichero = await LoadAttachmentChunksAsync(
                connection,
                attachment.Id_Fichero,
                totalLength,
                chunkCount,
                cancellationToken);
        }
    }

    private static async Task<byte[]> LoadAttachmentChunksAsync(
        MySqlConnection connection,
        long attachmentId,
        long totalLength,
        int expectedChunkCount,
        CancellationToken cancellationToken)
    {
        if (totalLength < 0 || totalLength > int.MaxValue)
        {
            throw new InvalidOperationException(
                $"El adjunto {attachmentId} no puede reconstruirse en memoria. Tamano declarado: {totalLength} bytes.");
        }

        if (expectedChunkCount <= 0)
        {
            throw new InvalidOperationException(
                $"El adjunto {attachmentId} tiene un marcador de chunks invalido.");
        }

        const string sql = """
            SELECT chunk_index, content
            FROM complaint_attachment_chunks
            WHERE attachment_id = @attachmentId
            ORDER BY chunk_index ASC;
            """;

        var result = new byte[(int)totalLength];
        var offset = 0;
        var chunksRead = 0;

        await using var command = new MySqlCommand(sql, connection);
        command.Parameters.AddWithValue("@attachmentId", attachmentId);

        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        while (await reader.ReadAsync(cancellationToken))
        {
            var chunkIndex = GetInt32(reader, "chunk_index");
            if (chunkIndex != chunksRead)
            {
                throw new InvalidOperationException(
                    $"El adjunto {attachmentId} tiene chunks incompletos o desordenados.");
            }

            var chunk = GetBytes(reader, "content");
            if (offset + chunk.Length > result.Length)
            {
                throw new InvalidOperationException(
                    $"El adjunto {attachmentId} tiene mas datos de chunks que los declarados.");
            }

            Buffer.BlockCopy(chunk, 0, result, offset, chunk.Length);
            offset += chunk.Length;
            chunksRead++;
        }

        if (chunksRead != expectedChunkCount || offset != result.Length)
        {
            throw new InvalidOperationException(
                $"El adjunto {attachmentId} no tiene todos los chunks esperados.");
        }

        return result;
    }

    private static byte[] CreateChunkedAttachmentMarker(long totalLength, int chunkCount)
    {
        return Encoding.ASCII.GetBytes(
            $"chunked:v1:{totalLength.ToString(CultureInfo.InvariantCulture)}:{chunkCount.ToString(CultureInfo.InvariantCulture)}");
    }

    private static bool TryParseChunkedAttachmentMarker(
        byte[] content,
        out long totalLength,
        out int chunkCount)
    {
        totalLength = 0;
        chunkCount = 0;

        if (!content.AsSpan().StartsWith(ChunkedAttachmentPrefix))
        {
            return false;
        }

        var marker = Encoding.ASCII.GetString(content);
        var parts = marker.Split(':');
        return parts.Length == 4 &&
               long.TryParse(parts[2], NumberStyles.None, CultureInfo.InvariantCulture, out totalLength) &&
               int.TryParse(parts[3], NumberStyles.None, CultureInfo.InvariantCulture, out chunkCount);
    }

    private static int GetChunkCount(int length)
    {
        return (int)(((long)length + AttachmentChunkSizeBytes - 1) / AttachmentChunkSizeBytes);
    }

    private static DenunciasGestiona MapComplaint(IDataRecord record)
    {
        return new DenunciasGestiona
        {
            Id_RegistroDenuncia = GetInt32(record, "external_registry_id"),
            Id_Denuncia = GetInt32(record, "external_report_id"),
            Fecha = GetDateTime(record, "report_date_utc"),
            Expediente_Gestiona = GetString(record, "gestiona_file_url"),
            CodigoExpedienteGestiona = GetString(record, "gestiona_file_code"),
            Id_Persona_Gestiona = GetInt32(record, "gestiona_person_id"),
            Etiqueta = GetString(record, "tag"),
            Estado = GetString(record, "status_name"),
            Tipo_Denuncia = GetString(record, "complaint_type"),
            TipoDenunciante = GetString(record, "reporter_kind"),
            EsPersonaJuridica = GetBoolean(record, "is_legal_entity"),
            Nombre = GetString(record, "reporter_first_name"),
            PrimerApellido = GetString(record, "reporter_first_surname"),
            SegundoApellido = GetString(record, "reporter_second_surname"),
            Apellidos = GetString(record, "reporter_last_name"),
            RazonSocial = GetString(record, "reporter_business_name"),
            Sexo = GetString(record, "reporter_gender"),
            Dni = GetString(record, "reporter_document_id"),
            TipoDocumentoIdentificativo = GetString(record, "reporter_document_type"),
            PaisOrigen = GetString(record, "reporter_origin_country"),
            Asunto = GetString(record, "subject"),
            A_Quien_Denuncia = GetString(record, "accused_party"),
            DenunciadoDetalle = GetString(record, "accused_party_details"),
            Descripcion_Denuncia = GetString(record, "complaint_description"),
            Denunciado_Ante_Inst = GetString(record, "reported_to_institution"),
            OrganismoDenunciado = GetString(record, "reported_institution_details"),
            SolicitaProteccion = GetString(record, "requested_protection"),
            MedidasProteccionSolicitadas = GetString(record, "requested_protection_details"),
            Modalidad_Informacion = GetString(record, "information_mode"),
            Lugar_Hechos = GetString(record, "facts_location"),
            Fecha_Hechos = GetDateTime(record, "facts_date_utc"),
            AutorizaRemision = GetString(record, "forwarding_authorization"),
            PreferenciaRemision = GetString(record, "forwarding_personal_data_preference"),
            Notificacion_Preferencia = GetString(record, "notification_preference"),
            Notificacion_Electronica = GetString(record, "electronic_notification"),
            SeguimientoOnline = GetString(record, "online_tracking_preference"),
            NotificacionPostal = GetString(record, "postal_notification_preference"),
            Correo_Electronico = GetString(record, "email"),
            Notificacion_Sms = GetString(record, "sms_notification"),
            Condiciones = GetBoolean(record, "accepted_terms"),
            Comments = GetString(record, "comments"),
            Telefono = GetString(record, "phone"),
            Direccion = GetString(record, "address_line"),
            DireccionTipoVia = GetString(record, "address_road_type"),
            DireccionNumero = GetString(record, "address_number"),
            DireccionPiso = GetString(record, "address_floor"),
            DireccionPuerta = GetString(record, "address_door"),
            DireccionBloque = GetString(record, "address_block"),
            DireccionEscalera = GetString(record, "address_stair"),
            DireccionExtra = GetString(record, "address_extra"),
            Municipio = GetString(record, "municipality"),
            Provincia = GetString(record, "province"),
            CodigoPostal = GetString(record, "postal_code"),
            Pais = GetString(record, "country_code"),
            CamposFormularioJson = GetString(record, "form_fields_json"),
            TextoOriginalReport = GetString(record, "raw_report_text"),
            Confidencial = GetBoolean(record, "is_confidential"),
            EsActualizacion = GetBoolean(record, "is_update"),
            ProcedureId = GetGuid(record, "gestiona_procedure_id"),
            GroupId = GetGuid(record, "gestiona_group_id"),
            NombreDenuncia = GetString(record, "display_name"),
            EstadoDenuncia = GetString(record, "workflow_status"),
            ArchivoElegido = GetString(record, "selected_document_name"),
            FechaSubidaAGestiona = GetDateTime(record, "gestiona_uploaded_at_utc"),
            UltimaSubidaGestionaTipo = GetString(record, "gestiona_last_upload_type"),
            UltimoGrupoAsignadoGestiona = GetString(record, "gestiona_assigned_group"),
            PendingUpdateSource = GetString(record, "pending_update_source"),
            EnGestiona = GetBoolean(record, "is_in_gestiona"),
            EnRechazada = GetBoolean(record, "is_rejected"),
            KeyDate = GetNullableDateOnly(record, "key_date"),
            EncryptionScheme = GetString(record, "encryption_scheme"),
            EncryptedAtUtc = GetNullableDateTime(record, "encrypted_at_utc"),
        };
    }

    private static FicherosDenuncias MapAttachment(IDataRecord record)
    {
        return new FicherosDenuncias
        {
            Id_Fichero = Convert.ToInt32(record["id"]),
            Id_Tipo = GetInt32(record, "attachment_type_id"),
            Descripcion = GetNullableString(record, "description"),
            Fecha = GetDateTime(record, "attachment_date_utc"),
            Observaciones = GetString(record, "notes"),
            Id_Denuncia = GetInt32(record, "external_report_id"),
            NombreFichero = GetString(record, "original_file_name"),
            Fichero = GetBytes(record, "content"),
            ContentSha256 = GetString(record, "content_sha256"),
            Subido = GetBoolean(record, "uploaded_to_gestiona"),
            FechaSubida = GetNullableDateTime(record, "uploaded_at_utc"),
            KeyDate = GetNullableDateOnly(record, "key_date"),
            EncryptionScheme = GetString(record, "encryption_scheme"),
            EncryptedAtUtc = GetNullableDateTime(record, "encrypted_at_utc"),
        };
    }

    private static GestionaUploadHistoryEntry MapGestionaUploadHistory(IDataRecord record)
    {
        return new GestionaUploadHistoryEntry(
            Convert.ToInt64(record["id"]),
            GetInt32(record, "external_report_id"),
            GetString(record, "gestiona_file_url"),
            GetString(record, "gestiona_file_code"),
            GetString(record, "upload_type"),
            ExtractGroupCode(GetString(record, "assigned_group_code")),
            GetString(record, "uploaded_by_username"),
            GetDateTime(record, "uploaded_at_utc"),
            GetString(record, "subject"),
            GetString(record, "document_names"));
    }

    private static string ExtractGroupCode(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }

        var trimmed = value.Trim();
        var code = new string(trimmed.TakeWhile(char.IsDigit).ToArray());
        return string.IsNullOrWhiteSpace(code) ? trimmed : code;
    }

    private static object ToDbDate(DateTime value)
    {
        return value == DateTime.MinValue ? DBNull.Value : value;
    }

    private static object ToDbDate(DateTime? value)
    {
        return value is null || value == DateTime.MinValue ? DBNull.Value : value.Value;
    }

    private static object ToDbDate(DateOnly? value)
    {
        return value is null ? DBNull.Value : value.Value.ToDateTime(TimeOnly.MinValue);
    }

    private static object ToDbGuid(Guid value)
    {
        return value == Guid.Empty ? DBNull.Value : value.ToString();
    }

    private static object ToDbStringOrNull(string? value)
    {
        return string.IsNullOrWhiteSpace(value) ? DBNull.Value : value;
    }

    private static string GetString(IDataRecord record, string columnName)
    {
        var ordinal = record.GetOrdinal(columnName);
        if (record.IsDBNull(ordinal))
        {
            return string.Empty;
        }

        return ConvertRecordValueToString(record.GetValue(ordinal)) ?? string.Empty;
    }

    private static string? GetNullableString(IDataRecord record, string columnName)
    {
        var ordinal = record.GetOrdinal(columnName);
        return record.IsDBNull(ordinal) ? null : ConvertRecordValueToString(record.GetValue(ordinal));
    }

    private static int GetInt32(IDataRecord record, string columnName)
    {
        var ordinal = record.GetOrdinal(columnName);
        return record.IsDBNull(ordinal) ? 0 : record.GetInt32(ordinal);
    }

    private static bool GetBoolean(IDataRecord record, string columnName)
    {
        var ordinal = record.GetOrdinal(columnName);
        return !record.IsDBNull(ordinal) && record.GetBoolean(ordinal);
    }

    private static DateTime GetDateTime(IDataRecord record, string columnName)
    {
        var ordinal = record.GetOrdinal(columnName);
        return record.IsDBNull(ordinal) ? DateTime.MinValue : record.GetDateTime(ordinal);
    }

    private static DateTime? GetNullableDateTime(IDataRecord record, string columnName)
    {
        var ordinal = record.GetOrdinal(columnName);
        return record.IsDBNull(ordinal) ? null : record.GetDateTime(ordinal);
    }

    private static DateOnly? GetNullableDateOnly(IDataRecord record, string columnName)
    {
        var ordinal = record.GetOrdinal(columnName);
        return record.IsDBNull(ordinal) ? null : DateOnly.FromDateTime(record.GetDateTime(ordinal));
    }

    private static Guid GetGuid(IDataRecord record, string columnName)
    {
        var ordinal = record.GetOrdinal(columnName);
        if (record.IsDBNull(ordinal))
        {
            return Guid.Empty;
        }

        var value = record.GetValue(ordinal);
        return value switch
        {
            Guid guid => guid,
            byte[] bytes when bytes.Length == 16 => new Guid(bytes),
            _ when Guid.TryParse(ConvertRecordValueToString(value), out var result) => result,
            _ => Guid.Empty
        };
    }

    private static byte[] GetBytes(IDataRecord record, string columnName)
    {
        var ordinal = record.GetOrdinal(columnName);
        if (record.IsDBNull(ordinal))
        {
            return [];
        }

        return (byte[])record.GetValue(ordinal);
    }

    private static string DetectMimeType(string? fileName)
    {
        var extension = Path.GetExtension(fileName ?? string.Empty).ToLowerInvariant();
        return extension switch
        {
            ".pdf" => "application/pdf",
            ".txt" => "text/plain",
            ".jpg" or ".jpeg" => "image/jpeg",
            ".png" => "image/png",
            ".gif" => "image/gif",
            ".doc" => "application/msword",
            ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
            ".xls" => "application/vnd.ms-excel",
            ".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            _ => "application/octet-stream",
        };
    }

    private static string ComputeSha256Hex(byte[] content)
    {
        var hash = SHA256.HashData(content);
        return Convert.ToHexString(hash).ToLowerInvariant();
    }

    private static string? ConvertRecordValueToString(object? value)
    {
        return value switch
        {
            null or DBNull => null,
            string text => text,
            Guid guid => guid.ToString(),
            byte[] bytes when bytes.Length == 16 => new Guid(bytes).ToString(),
            IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
            _ => value.ToString()
        };
    }
}
