using System.IO.Compression;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using ApiDenuncias.Helpers;
using GestionaDenuncias.Shared.Models;

namespace ApiDenuncias.Services;

public sealed class DenunciaInboxService
{
    private static readonly HashSet<string> BlockedAttachmentExtensions = new(StringComparer.OrdinalIgnoreCase)
    {
        ".ade",
        ".adp",
        ".apk",
        ".app",
        ".appx",
        ".bat",
        ".cmd",
        ".com",
        ".cpl",
        ".dll",
        ".exe",
        ".gadget",
        ".hta",
        ".ins",
        ".iso",
        ".jar",
        ".js",
        ".jse",
        ".lnk",
        ".msc",
        ".msi",
        ".msp",
        ".mst",
        ".pif",
        ".ps1",
        ".ps1xml",
        ".ps2",
        ".ps2xml",
        ".psc1",
        ".psc2",
        ".reg",
        ".scr",
        ".sh",
        ".sys",
        ".vb",
        ".vbe",
        ".vbs",
        ".ws",
        ".wsc",
        ".wsf",
        ".wsh"
    };

    private readonly IGestionaService _gestionaService;
    private readonly GestionaExpedienteExceptionStore _gestionaExceptionStore;
    private readonly IDenunciaStore _denunciaStore;
    private readonly IFilteredDenunciaStore _filteredDenunciaStore;
    private readonly ILogger<DenunciaInboxService> _logger;

    public DenunciaInboxService(
        IGestionaService gestionaService,
        GestionaExpedienteExceptionStore gestionaExceptionStore,
        IDenunciaStore denunciaStore,
        IFilteredDenunciaStore filteredDenunciaStore,
        ILogger<DenunciaInboxService> logger)
    {
        _gestionaService = gestionaService;
        _gestionaExceptionStore = gestionaExceptionStore;
        _denunciaStore = denunciaStore;
        _filteredDenunciaStore = filteredDenunciaStore;
        _logger = logger;
    }

    public async Task EnsureStorageReadyAsync(CancellationToken cancellationToken = default)
    {
        await _denunciaStore.EnsureSchemaAsync(cancellationToken);
        await _gestionaExceptionStore.EnsureSchemaAsync(cancellationToken);
    }

    public async Task<ImportSummary> ImportFromGlobalLeaksAsync(
        FileDownloadResult reportDownload,
        FileDownloadResult? jsonDownload,
        ReportDetailDto? reportDetail,
        CancellationToken cancellationToken = default)
    {
        await EnsureStorageReadyAsync(cancellationToken);

        var sourceName = string.IsNullOrWhiteSpace(reportDownload.FileName)
            ? $"report-{Guid.NewGuid():N}"
            : reportDownload.FileName;
        if (sourceName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
        {
            sourceName = Path.GetFileNameWithoutExtension(sourceName);
        }

        var json = jsonDownload is null
            ? null
            : Encoding.UTF8.GetString(jsonDownload.Content);

        try
        {
            var result = await ProcessGlobalLeaksPackageAsync(reportDownload.Content, sourceName, json, reportDetail, cancellationToken);
            return new ImportSummary(
                1,
                result.ImportedCount,
                [],
                result.ImportedCount > 0 ? [result.ComplaintId] : [],
                result.Warnings);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importando denuncia desde GlobalLeaks {SourceName}", sourceName);
            return new ImportSummary(1, 0, [ToUserImportErrorMessage(sourceName, ex)]);
        }
    }

    private static string ToUserImportErrorMessage(string sourceName, Exception ex)
    {
        var message = ex.Message ?? string.Empty;

        if (ex is EncryptionKeyUnavailableException ||
            message.Contains("encryption_keys", StringComparison.OrdinalIgnoreCase) ||
            message.Contains("clave de cifrado", StringComparison.OrdinalIgnoreCase))
        {
            return $"{sourceName}: no hay una clave de cifrado activa para guardar la denuncia. Avisa al equipo tecnico.";
        }

        if (message.Contains("max_allowed_packet", StringComparison.OrdinalIgnoreCase) ||
            message.Contains("Error submitting", StringComparison.OrdinalIgnoreCase))
        {
            return $"{sourceName}: la denuncia contiene un adjunto demasiado grande para guardarse. Revisa los adjuntos o consulta con soporte.";
        }

        if (message.Contains("Connection must be Open", StringComparison.OrdinalIgnoreCase) ||
            message.Contains("current state is Closed", StringComparison.OrdinalIgnoreCase))
        {
            return $"{sourceName}: se ha perdido la conexion mientras se guardaba la denuncia. Vuelve a intentarlo.";
        }

        if (ex is InvalidDataException)
        {
            return $"{sourceName}: el paquete descargado de GlobalLeaks no se ha podido leer correctamente. Vuelve a intentarlo.";
        }

        if (ex is InvalidOperationException && IsUserFacingImportMessage(message))
        {
            return $"{sourceName}: {message}";
        }

        return $"{sourceName}: no se ha podido importar la denuncia. Vuelve a intentarlo; si se repite, avisa al equipo tecnico.";
    }

    private static bool IsUserFacingImportMessage(string message)
        => message.StartsWith("El paquete de GlobalLeaks", StringComparison.OrdinalIgnoreCase) ||
           message.StartsWith("El report viene", StringComparison.OrdinalIgnoreCase) ||
           message.StartsWith("El archivo", StringComparison.OrdinalIgnoreCase) ||
           message.StartsWith("Denuncia no disponible", StringComparison.OrdinalIgnoreCase);

    private async Task<ProcessPackageResult> ProcessGlobalLeaksPackageAsync(
        byte[] packageBytes,
        string sourceName,
        string? globalLeaksJson,
        ReportDetailDto? reportDetail,
        CancellationToken cancellationToken)
    {
        using var packageStream = new MemoryStream(packageBytes, writable: false);
        using var archive = new ZipArchive(packageStream, ZipArchiveMode.Read, leaveOpen: false);

        var reportEntry = FindReportEntry(archive);

        if (reportEntry is null)
        {
            var entries = archive.Entries
                .Where(entry => !string.IsNullOrWhiteSpace(entry.Name))
                .Select(entry => NormalizeEntryPath(entry.FullName))
                .Take(30)
                .ToArray();

            throw new InvalidOperationException(
                entries.Length == 0
                    ? "El paquete de GlobalLeaks no contiene ficheros."
                    : $"El paquete de GlobalLeaks no contiene un report reconocible. Ficheros encontrados: {string.Join(", ", entries)}");
        }

        var reportIsPdf = IsPdfEntry(reportEntry);
        var reportText = reportIsPdf
            ? string.Empty
            : await ReadEntryTextAsync(reportEntry, cancellationToken);
        var denuncia = reportIsPdf
            ? new DenunciasGestiona()
            : ReportParser.ParseReport(reportText);

        if (!string.IsNullOrWhiteSpace(globalLeaksJson))
        {
            GlobalLeaksJsonEnricher.Enrich(denuncia, globalLeaksJson);
        }
        else if (reportIsPdf)
        {
            throw new InvalidOperationException(
                "El report viene en PDF y no se ha recibido el JSON de GlobalLeaks necesario para extraer los datos de la denuncia.");
        }

        if (denuncia.Id_Denuncia == 0)
        {
            denuncia.Id_Denuncia = TryReadIdFromReport(reportText);
        }

        if (denuncia.Id_Denuncia == 0)
        {
            throw new InvalidOperationException(
                $"No se ha podido determinar el identificador de la denuncia en {sourceName}.");
        }

        if (reportIsPdf)
        {
            reportText = BuildSyntheticReportText(denuncia);
            denuncia.TextoOriginalReport = reportText;
        }

        denuncia.ProcedureId = Guid.Empty;
        denuncia.GroupId = Guid.Empty;
        if (string.IsNullOrWhiteSpace(denuncia.Expediente_Gestiona))
        {
            denuncia.Expediente_Gestiona = "Pendiente";
        }

        var readFilesResult = await ReadFilesFromArchiveAsync(
            archive,
            reportEntry,
            denuncia.Id_Denuncia,
            denuncia.Fecha,
            reportDetail,
            cancellationToken);
        await MergeComplaintAsync(denuncia, cancellationToken);
        var storedComplaintBeforeFiles = await _denunciaStore.GetDenunciaByIdAsync(denuncia.Id_Denuncia, cancellationToken);
        var existingFiles = IsAlreadyUploadedToGestiona(storedComplaintBeforeFiles)
            ? await GetFicherosMetadataByDenunciaForImportAsync(denuncia.Id_Denuncia, cancellationToken)
            : [];
        PreserveKnownUploadedReportStatus(readFilesResult.Files, existingFiles, storedComplaintBeforeFiles);
        await MergeFilesAsync(readFilesResult.Files, cancellationToken);

        var warnings = readFilesResult.Warnings.ToList();
        var storedComplaint = await _denunciaStore.GetDenunciaByIdAsync(denuncia.Id_Denuncia, cancellationToken);
        if (IsAlreadyUploadedToGestiona(storedComplaint))
        {
            var storedFiles = await GetFicherosMetadataByDenunciaForImportAsync(denuncia.Id_Denuncia, cancellationToken);
            if (!HasPendingFilesForGestiona(storedFiles))
            {
                storedComplaint!.EsActualizacion = false;
                storedComplaint.EnGestiona = true;
                await _denunciaStore.UpsertDenunciaAsync(storedComplaint, cancellationToken);

                warnings.Add($"La denuncia #{denuncia.Id_Denuncia} ya esta en Gestiona y no tiene documentos nuevos pendientes de subir.");
                return new ProcessPackageResult(denuncia.Id_Denuncia, 0, warnings);
            }
        }

        return new ProcessPackageResult(denuncia.Id_Denuncia, 1, warnings);
    }

    private Task<List<FicherosDenuncias>> GetFicherosMetadataByDenunciaForImportAsync(
        int denunciaId,
        CancellationToken cancellationToken)
        => _filteredDenunciaStore.GetFicherosMetadataByDenunciaAsync(denunciaId, cancellationToken);

    private async Task<ReadFilesResult> ReadFilesFromArchiveAsync(
        ZipArchive archive,
        ZipArchiveEntry reportEntry,
        int denunciaId,
        DateTime reportDateUtc,
        ReportDetailDto? reportDetail,
        CancellationToken cancellationToken)
    {
        var warnings = new List<string>();
        var files = new List<FicherosDenuncias>
        {
            new(
                id_Fichero: 0,
                id_Tipo: 1,
                descripcion: IsPdfEntry(reportEntry) ? "report.pdf original" : "report.txt original",
                fecha: reportDateUtc == DateTime.MinValue
                    ? GetEntryFallbackDateUtc(reportEntry)
                    : NormalizeUtc(reportDateUtc),
                observaciones: "",
                id_Denuncia: denunciaId,
                nombreFichero: IsPdfEntry(reportEntry) ? "report.pdf" : "report.txt",
                fichero: await ReadEntryBytesAsync(reportEntry, cancellationToken))
        };

        foreach (var entry in archive.Entries.Where(entry => IsSupportedAttachmentEntry(entry) && !IsSameEntry(entry, reportEntry)))
        {
            if (IsBlockedAttachmentEntry(entry))
            {
                warnings.Add(BuildBlockedAttachmentWarning(entry));
                continue;
            }

            files.Add(new FicherosDenuncias(
                id_Fichero: 0,
                id_Tipo: 1,
                descripcion: null,
                fecha: ResolveAttachmentDateUtc(entry, reportDetail),
                observaciones: "",
                id_Denuncia: denunciaId,
                nombreFichero: Path.GetFileName(entry.FullName),
                fichero: await ReadEntryBytesAsync(entry, cancellationToken)));
        }

        return new ReadFilesResult(files, warnings);
    }

    private async Task MergeComplaintAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken)
    {
        DenunciasGestiona? existing = null;
        try
        {
            existing = await _denunciaStore.GetDenunciaByIdAsync(denuncia.Id_Denuncia, cancellationToken);
        }
        catch (EncryptedDataPurgedException ex)
        {
            _logger.LogInformation(
                ex,
                "La denuncia {DenunciaId} existe con datos purgados. Se reimportara como nueva copia cifrada con la clave activa.",
                denuncia.Id_Denuncia);
        }

        if (existing is not null)
        {
            CopyComplaintData(existing, denuncia);
            await CompleteGestionaStatusAsync(existing, existing, cancellationToken);
            await _denunciaStore.UpsertDenunciaAsync(existing, cancellationToken);
            return;
        }

        await CompleteGestionaStatusAsync(denuncia, null, cancellationToken);
        await _denunciaStore.UpsertDenunciaAsync(denuncia, cancellationToken);
    }

    private Task MergeFilesAsync(List<FicherosDenuncias> nuevosFicheros, CancellationToken cancellationToken)
    {
        return _denunciaStore.UpsertFicherosAsync(nuevosFicheros, cancellationToken);
    }

    private static void PreserveKnownUploadedReportStatus(
        List<FicherosDenuncias> newFiles,
        IReadOnlyList<FicherosDenuncias> existingFiles,
        DenunciasGestiona? storedComplaint)
    {
        if (!IsAlreadyUploadedToGestiona(storedComplaint) || existingFiles.Count == 0)
        {
            return;
        }

        var existingReportHashes = existingFiles
            .Where(file => file.EsReport && !string.IsNullOrWhiteSpace(file.ContentSha256))
            .GroupBy(file => file.ContentSha256, StringComparer.OrdinalIgnoreCase)
            .ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase);

        foreach (var report in newFiles.Where(file => file.EsReport))
        {
            var hash = string.IsNullOrWhiteSpace(report.ContentSha256)
                ? ComputeSha256Hex(report.Fichero ?? [])
                : report.ContentSha256.Trim().ToLowerInvariant();
            report.ContentSha256 = hash;

            if (!existingReportHashes.TryGetValue(hash, out var existingReport))
            {
                continue;
            }

            report.Subido = true;
            report.FechaSubida = existingReport.FechaSubida ??
                                  (storedComplaint!.FechaSubidaAGestiona == DateTime.MinValue
                                      ? null
                                      : storedComplaint.FechaSubidaAGestiona);
        }
    }

    private static bool HasPendingFilesForGestiona(IReadOnlyList<FicherosDenuncias> files)
    {
        var plannedHashes = files
            .Where(file => file.Subido && !string.IsNullOrWhiteSpace(file.ContentSha256))
            .Select(file => file.ContentSha256)
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        foreach (var file in files.Where(file => !file.Subido))
        {
            if (string.IsNullOrWhiteSpace(file.ContentSha256))
            {
                return true;
            }

            if (plannedHashes.Add(file.ContentSha256))
            {
                return true;
            }
        }

        return false;
    }

    private static string ComputeSha256Hex(byte[] content)
        => Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant();

    private async Task CompleteGestionaStatusAsync(
        DenunciasGestiona denuncia,
        CancellationToken cancellationToken)
    {
        await CompleteGestionaStatusAsync(denuncia, null, cancellationToken);
    }

    private async Task CompleteGestionaStatusAsync(
        DenunciasGestiona denuncia,
        DenunciasGestiona? storedComplaint,
        CancellationToken cancellationToken)
    {
        if (TryApplyGestionaStatusFromStore(denuncia, storedComplaint))
        {
            return;
        }

        try
        {
            var match = await _gestionaService.BuscarExpedientePorIdEnAsuntoAsync(denuncia.Id_Denuncia);
            if (match is null)
            {
                match = await TryFindGestionaExceptionMatchAsync(denuncia, cancellationToken);
                if (match is null)
                {
                    ApplyPendingGestionaStatus(denuncia);
                    return;
                }
            }

            ApplyGestionaMatch(denuncia, match);
        }
        catch (Exception ex)
        {
            ApplyPendingGestionaStatus(denuncia);
            _logger.LogWarning(
                ex,
                "No se ha podido comprobar si la denuncia {DenunciaId} ya existia en Gestiona",
                denuncia.Id_Denuncia);
        }
    }

    private async Task<GestionaExpedienteInfo?> TryFindGestionaExceptionMatchAsync(
        DenunciasGestiona denuncia,
        CancellationToken cancellationToken)
    {
        var exception = await _gestionaExceptionStore.FindAsync(denuncia, cancellationToken);
        if (exception is null)
        {
            return null;
        }

        var match = await _gestionaService.BuscarExpedientePorCodigoAsync(exception.GestionaFileCode);
        if (match is null)
        {
            _logger.LogWarning(
                "La denuncia {DenunciaId} tiene excepcion Gestiona con codigo {GestionaFileCode}, pero no se ha encontrado expediente en Gestiona.",
                denuncia.Id_Denuncia,
                exception.GestionaFileCode);
        }

        return match;
    }

    private static void ApplyGestionaMatch(
        DenunciasGestiona denuncia,
        GestionaExpedienteInfo match)
    {
        denuncia.EsActualizacion = true;
        denuncia.EnGestiona = true;
        denuncia.Expediente_Gestiona = match.FileUrl;
        denuncia.CodigoExpedienteGestiona = match.CodigoExpediente ?? string.Empty;
        denuncia.NombreDenuncia = match.FreeTitle ?? $"Denuncia {denuncia.Id_Denuncia}-CD";
    }

    private static bool TryApplyGestionaStatusFromStore(
        DenunciasGestiona target,
        DenunciasGestiona? storedComplaint)
    {
        if (!IsAlreadyUploadedToGestiona(storedComplaint))
        {
            return false;
        }

        target.EsActualizacion = true;
        target.EnGestiona = true;
        target.Expediente_Gestiona = string.IsNullOrWhiteSpace(storedComplaint!.Expediente_Gestiona)
            ? "Pendiente"
            : storedComplaint.Expediente_Gestiona;
        target.CodigoExpedienteGestiona = storedComplaint.CodigoExpedienteGestiona ?? string.Empty;

        if (!string.IsNullOrWhiteSpace(storedComplaint.NombreDenuncia))
        {
            target.NombreDenuncia = storedComplaint.NombreDenuncia;
        }

        if (storedComplaint.FechaSubidaAGestiona != DateTime.MinValue)
        {
            target.FechaSubidaAGestiona = storedComplaint.FechaSubidaAGestiona;
        }

        return !string.IsNullOrWhiteSpace(target.CodigoExpedienteGestiona);
    }

    private static void ApplyPendingGestionaStatus(DenunciasGestiona denuncia)
    {
        denuncia.EsActualizacion = false;
        denuncia.EnGestiona = false;
        denuncia.Expediente_Gestiona = "Pendiente";
        denuncia.CodigoExpedienteGestiona = string.Empty;
        if (string.IsNullOrWhiteSpace(denuncia.NombreDenuncia))
        {
            denuncia.NombreDenuncia = $"Denuncia {denuncia.Id_Denuncia}-CD";
        }
    }

    private static bool IsAlreadyUploadedToGestiona(DenunciasGestiona? denuncia)
    {
        if (denuncia is null)
        {
            return false;
        }

        if (denuncia.EnGestiona || denuncia.FechaSubidaAGestiona != DateTime.MinValue)
        {
            return true;
        }

        return !string.IsNullOrWhiteSpace(denuncia.Expediente_Gestiona) &&
               !string.Equals(denuncia.Expediente_Gestiona, "Pendiente", StringComparison.OrdinalIgnoreCase);
    }

    private static void CopyComplaintData(DenunciasGestiona target, DenunciasGestiona source)
    {
        target.Fecha = source.Fecha;
        target.Etiqueta = source.Etiqueta;
        target.Estado = source.Estado;
        target.Tipo_Denuncia = source.Tipo_Denuncia;
        target.TipoDenunciante = source.TipoDenunciante;
        target.EsPersonaJuridica = source.EsPersonaJuridica;
        target.Confidencial = source.Confidencial;
        target.Nombre = source.Nombre;
        target.PrimerApellido = source.PrimerApellido;
        target.SegundoApellido = source.SegundoApellido;
        target.Apellidos = source.Apellidos;
        target.RazonSocial = source.RazonSocial;
        target.Sexo = source.Sexo;
        target.Dni = source.Dni;
        target.TipoDocumentoIdentificativo = source.TipoDocumentoIdentificativo;
        target.PaisOrigen = source.PaisOrigen;
        target.Asunto = source.Asunto;
        target.A_Quien_Denuncia = source.A_Quien_Denuncia;
        target.DenunciadoDetalle = source.DenunciadoDetalle;
        target.Descripcion_Denuncia = source.Descripcion_Denuncia;
        target.Denunciado_Ante_Inst = source.Denunciado_Ante_Inst;
        target.OrganismoDenunciado = source.OrganismoDenunciado;
        target.SolicitaProteccion = source.SolicitaProteccion;
        target.MedidasProteccionSolicitadas = source.MedidasProteccionSolicitadas;
        target.Modalidad_Informacion = source.Modalidad_Informacion;
        target.Lugar_Hechos = source.Lugar_Hechos;
        target.Fecha_Hechos = source.Fecha_Hechos;
        target.AutorizaRemision = source.AutorizaRemision;
        target.PreferenciaRemision = source.PreferenciaRemision;
        target.Notificacion_Preferencia = source.Notificacion_Preferencia;
        target.Notificacion_Electronica = source.Notificacion_Electronica;
        target.SeguimientoOnline = source.SeguimientoOnline;
        target.NotificacionPostal = source.NotificacionPostal;
        target.Correo_Electronico = source.Correo_Electronico;
        target.Notificacion_Sms = source.Notificacion_Sms;
        target.Condiciones = source.Condiciones;
        target.Comments = source.Comments;
        target.Telefono = source.Telefono;
        target.Direccion = source.Direccion;
        target.DireccionTipoVia = source.DireccionTipoVia;
        target.DireccionNumero = source.DireccionNumero;
        target.DireccionPiso = source.DireccionPiso;
        target.DireccionPuerta = source.DireccionPuerta;
        target.DireccionBloque = source.DireccionBloque;
        target.DireccionEscalera = source.DireccionEscalera;
        target.DireccionExtra = source.DireccionExtra;
        target.Municipio = source.Municipio;
        target.Provincia = source.Provincia;
        target.CodigoPostal = source.CodigoPostal;
        target.Pais = source.Pais;
        target.CamposFormularioJson = source.CamposFormularioJson;
        target.TextoOriginalReport = source.TextoOriginalReport;
    }

    private static bool IsSupportedAttachmentEntry(ZipArchiveEntry entry)
    {
        if (string.IsNullOrWhiteSpace(entry.Name))
        {
            return false;
        }

        var normalized = NormalizeEntryPath(entry.FullName);
        return IsDirectChildOf(normalized, "files") || IsDirectChildOf(normalized, "files_attached_from_recipients");
    }

    private static DateTime ResolveAttachmentDateUtc(ZipArchiveEntry entry, ReportDetailDto? reportDetail)
    {
        if (reportDetail is not null)
        {
            var normalized = NormalizeEntryPath(entry.FullName);
            var preferredFiles = IsDirectChildOf(normalized, "files_attached_from_recipients")
                ? reportDetail.ReceiverFiles
                : reportDetail.WhistleblowerFiles;

            var detailDate = TryFindReportFileDateUtc(entry, preferredFiles) ??
                             TryFindReportFileDateUtc(entry, reportDetail.WhistleblowerFiles.Concat(reportDetail.ReceiverFiles));
            if (detailDate is not null)
            {
                return detailDate.Value;
            }
        }

        return GetEntryFallbackDateUtc(entry);
    }

    private static DateTime? TryFindReportFileDateUtc(ZipArchiveEntry entry, IEnumerable<ReportFileDto> reportFiles)
    {
        var entryName = NormalizeFileName(entry.Name);
        var candidates = reportFiles
            .Where(file => !string.IsNullOrWhiteSpace(file.Name) &&
                           string.Equals(NormalizeFileName(file.Name), entryName, StringComparison.OrdinalIgnoreCase))
            .OrderByDescending(file => file.Size is null || file.Size == entry.Length)
            .ToList();

        foreach (var candidate in candidates)
        {
            var parsed = TryParseGlobalLeaksDateUtc(candidate.CreationDate);
            if (parsed is not null)
            {
                return parsed;
            }
        }

        return null;
    }

    private static DateTime? TryParseGlobalLeaksDateUtc(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        var normalized = value.Replace("Z", "+00:00", StringComparison.Ordinal);
        return DateTimeOffset.TryParse(
            normalized,
            CultureInfo.InvariantCulture,
            DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
            out var parsed)
            ? parsed.UtcDateTime
            : null;
    }

    private static DateTime GetEntryFallbackDateUtc(ZipArchiveEntry entry)
    {
        var utcDate = entry.LastWriteTime.UtcDateTime;
        return utcDate == DateTime.MinValue
            ? DateTime.UtcNow
            : utcDate;
    }

    private static DateTime NormalizeUtc(DateTime value)
        => value.Kind switch
        {
            DateTimeKind.Utc => value,
            DateTimeKind.Local => value.ToUniversalTime(),
            _ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
        };

    private static string NormalizeFileName(string fileName)
        => Path.GetFileName(fileName.Replace('\\', '/')).Trim();

    private static bool IsBlockedAttachmentEntry(ZipArchiveEntry entry)
    {
        var extension = Path.GetExtension(entry.Name);
        return !string.IsNullOrWhiteSpace(extension) &&
               BlockedAttachmentExtensions.Contains(extension);
    }

    private static string BuildBlockedAttachmentWarning(ZipArchiveEntry entry)
    {
        var fileName = Path.GetFileName(entry.FullName);
        var extension = Path.GetExtension(entry.Name);
        return $"Se ha detectado y eliminado el archivo '{fileName}' porque la extension '{extension}' no esta permitida.";
    }

    private static ZipArchiveEntry? FindReportEntry(ZipArchive archive)
    {
        return archive.Entries.FirstOrDefault(IsReportEntry);
    }

    private static bool IsReportEntry(ZipArchiveEntry entry)
    {
        if (string.IsNullOrWhiteSpace(entry.Name))
        {
            return false;
        }

        var fileName = Path.GetFileName(NormalizeEntryPath(entry.FullName));
        if (string.Equals(fileName, "report.txt", StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        var nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
        var extension = Path.GetExtension(fileName);
        return (extension.Equals(".txt", StringComparison.OrdinalIgnoreCase) ||
                extension.Equals(".pdf", StringComparison.OrdinalIgnoreCase)) &&
               nameWithoutExtension.StartsWith("report", StringComparison.OrdinalIgnoreCase);
    }

    private static bool IsPdfEntry(ZipArchiveEntry entry)
    {
        return Path.GetExtension(entry.Name).Equals(".pdf", StringComparison.OrdinalIgnoreCase);
    }

    private static bool IsSameEntry(ZipArchiveEntry left, ZipArchiveEntry right)
    {
        return string.Equals(
            NormalizeEntryPath(left.FullName),
            NormalizeEntryPath(right.FullName),
            StringComparison.OrdinalIgnoreCase);
    }

    private static bool IsDirectChildOf(string normalizedEntryPath, string rootFolder)
    {
        if (!normalizedEntryPath.StartsWith(rootFolder + "/", StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        var remaining = normalizedEntryPath[(rootFolder.Length + 1)..];
        return !remaining.Contains('/');
    }

    private static string NormalizeEntryPath(string path)
    {
        return path.Replace('\\', '/').Trim('/');
    }

    private static async Task<string> ReadEntryTextAsync(
        ZipArchiveEntry entry,
        CancellationToken cancellationToken)
    {
        var bytes = await ReadEntryBytesAsync(entry, cancellationToken);
        return Encoding.UTF8.GetString(bytes);
    }

    private static async Task<byte[]> ReadEntryBytesAsync(
        ZipArchiveEntry entry,
        CancellationToken cancellationToken)
    {
        await using var entryStream = entry.Open();
        using var memory = new MemoryStream();
        await entryStream.CopyToAsync(memory, cancellationToken);
        return memory.ToArray();
    }

    private static int TryReadIdFromReport(string reportText)
    {
        using var reader = new StringReader(reportText);
        string? line;

        while ((line = reader.ReadLine()) is not null)
        {
            line = line.Trim();
            if (line.StartsWith("ID:", StringComparison.OrdinalIgnoreCase) &&
                int.TryParse(line[3..].Trim(), out var id))
            {
                return id;
            }
        }

        return 0;
    }

    private static string BuildSyntheticReportText(DenunciasGestiona denuncia)
    {
        var builder = new StringBuilder();
        builder.AppendLine($"ID: {denuncia.Id_Denuncia}");
        if (denuncia.Fecha != DateTime.MinValue)
        {
            builder.AppendLine($"Fecha: {denuncia.Fecha:O}");
        }

        AppendMetadata(builder, "Etiqueta", denuncia.Etiqueta);
        AppendMetadata(builder, "Estado", denuncia.Estado);
        builder.AppendLine();

        AppendSection(builder, "Datos del denunciante",
            ("Indique si actúa como persona física o en representación de una persona jurídica.", denuncia.TipoDenunciante),
            ("Nombre", denuncia.Nombre),
            ("1º Apellido", denuncia.PrimerApellido),
            ("2º Apellido", denuncia.SegundoApellido),
            ("Razón social", denuncia.RazonSocial),
            ("SEXO", denuncia.Sexo),
            ("CONTACTO TELEFÓNICO", denuncia.Telefono),
            ("País de Origen", denuncia.PaisOrigen),
            ("NIF (DNI, NIE)", denuncia.Dni));

        AppendSection(builder, "Descripción",
            ("Asunto", denuncia.Asunto),
            ("¿A quién denuncia?", denuncia.A_Quien_Denuncia),
            ("Describa su denuncia", denuncia.Descripcion_Denuncia),
            ("¿Ha denunciado estos hechos ante otras instituciones u órganos?", denuncia.Denunciado_Ante_Inst),
            ("POR FAVOR. INDIQUE EL ORGANISMO O LA INSTITUCION DONDE HA DENUNCIADO LOS HECHOS", denuncia.OrganismoDenunciado),
            ("¿Solicita medidas concretas de protección?", denuncia.SolicitaProteccion),
            ("DESCRIBA LAS MEDIDAS DE PROTECCIÓN SOLICITADAS", denuncia.MedidasProteccionSolicitadas),
            ("Lugar en el que ocurrieron los hechos que denuncia", denuncia.Lugar_Hechos),
            ("Fecha de los hechos que denuncia", denuncia.Fecha_Hechos == DateTime.MinValue ? string.Empty : denuncia.Fecha_Hechos.ToString("dd/MM/yyyy")),
            ("Autorización para remitir su denuncia", denuncia.AutorizaRemision),
            ("En tal caso, ¿desea que su denuncia se remita anonimizada (sin datos personales)?", denuncia.PreferenciaRemision));

        AppendSection(builder, "Preferencias de notificación",
            ("Preferencia de notificación", denuncia.Notificacion_Preferencia),
            ("Notificaciones Electrónicas", denuncia.Notificacion_Electronica),
            ("Correo electrónico", denuncia.Correo_Electronico),
            ("Seguimiento Online", denuncia.SeguimientoOnline),
            ("Autorizo recibir notificaciones vía Correo Postal", denuncia.NotificacionPostal),
            ("Provincia", denuncia.Provincia),
            ("Tipo de vía", denuncia.DireccionTipoVia),
            ("Nombre de la vía", denuncia.Direccion),
            ("Código Postal", denuncia.CodigoPostal),
            ("Localidad", denuncia.Municipio),
            ("Número/Km", denuncia.DireccionNumero),
            ("Bloque", denuncia.DireccionBloque),
            ("Escalera", denuncia.DireccionEscalera),
            ("Planta", denuncia.DireccionPiso),
            ("Puerta", denuncia.DireccionPuerta),
            ("Extra", denuncia.DireccionExtra));

        if (!string.IsNullOrWhiteSpace(denuncia.Comments))
        {
            builder.AppendLine("{Messages}");
            builder.AppendLine(denuncia.Comments);
        }

        return builder.ToString();
    }

    private static void AppendMetadata(StringBuilder builder, string label, string? value)
    {
        if (!string.IsNullOrWhiteSpace(value))
        {
            builder.AppendLine($"{label}: {value}");
        }
    }

    private static void AppendSection(StringBuilder builder, string section, params (string Label, string? Value)[] fields)
    {
        builder.AppendLine(section);
        foreach (var (label, value) in fields)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                continue;
            }

            builder.AppendLine($"  {label}");
            builder.AppendLine($"    {value}");
        }

        builder.AppendLine();
    }

    private sealed record ProcessPackageResult(int ComplaintId, int ImportedCount, IReadOnlyList<string> Warnings);

    private sealed record ReadFilesResult(List<FicherosDenuncias> Files, IReadOnlyList<string> Warnings);
}

