@page "/Actualizaciones"
@rendermode InteractiveServer
@attribute [Authorize]
@using GestionaDenuncias.Shared.Models
@using System.Globalization
@using System.IO
@using System.Linq
@using System.Text

@using GestionaDenuncias.Shared.Helpers
@using GestionaDenunciasAN.Services
@attribute [StreamRendering]
@inject GestionaDenunciasAN.Models.UserState userState
@inject NavigationManager Navigation
@inject IHostEnvironment HostEnvironment
@inject IDenunciaStore DenunciaStore
@inject ApiDenunciasClient ApiDenuncias
@inject UiBusyService Busy
@inject IJSRuntime JSRuntime

<PageTitle>Actualizaciones</PageTitle>

<style>
    .pendientes-list {
        margin-top: 20px;
    }

    .collapse-card {
        margin-bottom: 1rem;
        border: 1px solid #ccc;
        border-radius: 6px;
        box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        overflow: hidden;
        transition: transform 0.3s ease, box-shadow 0.3s ease;
        background-color: #fff;
    }

        .collapse-card:hover {
            transform: scale(1.02);
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
        }

        .collapse-card .card-header {
            padding: 0.75rem 1.25rem;
            cursor: pointer;
            background-color: #f7f7f7;
            transition: background-color 0.3s ease;
        }

            .collapse-card .card-header:hover {
                background-color: #e2e2e2;
            }

    .section-heading {
        text-align: center;
        font-weight: bold;
        text-decoration: underline;
        font-size: 1.1em;
        margin: 1rem 0 0.5rem;
    }

    /* Tarjetas de actualización (azules) */
    .collapse-card.update-card {
        background-color: #e3f2fd;
    }

        .collapse-card.update-card > .card-header {
            background-color: #bbdefb;
        }

    .seleccionar-col {
        width: 50px;
        text-align: center;
        vertical-align: middle;
    }

    /* === Estética de modal igual que en Pendientes === */

    .custom-modal {
        background: rgba(0, 0, 0, 0.5);
    }

    .custom-modal-dialog {
        max-width: 500px;
        margin: 2rem auto;
    }

    .custom-modal-content {
        border-radius: 0.5rem;
        box-shadow: 0 5px 15px rgba(0,0,0,0.5);
        border: none;
    }

    .custom-modal-header {
        background-color: #007bff;
        color: white;
        border-top-left-radius: 0.5rem;
        border-top-right-radius: 0.5rem;
        padding: 1rem;
    }

    .custom-modal-body {
        padding: 1.5rem;
        background-color: #f8f9fa;
    }

    .custom-modal-footer {
        border-top: none;
        padding: 1rem;
    }

    .modal-section-heading {
        font-weight: bold;
        font-size: 1.1em;
        margin-bottom: 0.5rem;
        border-bottom: 2px solid #007bff;
        padding-bottom: 0.25rem;
    }
</style>

<h3>Actualizaciones</h3>

@if (!string.IsNullOrWhiteSpace(loadError))
{
    <div class="alert alert-danger">@loadError</div>
}
@if (!string.IsNullOrWhiteSpace(operationNotice))
{
    <div class="alert alert-warning">@operationNotice</div>
}

<input type="text"
       class="form-control"
       placeholder="Buscar actualizaciones..."
       @bind="busqueda"
       @bind:event="oninput"
       style="margin-bottom:20px;" />

@if (!hasLoaded)
{
    <div class="alert alert-info">Cargando datos...</div>
}
else if (actualizaciones == null || !actualizaciones.Any())
{
    <div class="alert alert-secondary">No hay actualizaciones pendientes.</div>
}
else
{
    <div class="pendientes-list">
        @foreach (var denuncia in actualizaciones.Where(d =>
            string.IsNullOrWhiteSpace(busqueda) ||
            d.Id_Denuncia.ToString().Contains(busqueda) ||
            (!string.IsNullOrEmpty(d.Asunto) && d.Asunto.Contains(busqueda, StringComparison.OrdinalIgnoreCase)) ||
            (!string.IsNullOrEmpty(d.Nombre) && d.Nombre.Contains(busqueda, StringComparison.OrdinalIgnoreCase)) ||
            (!string.IsNullOrEmpty(d.Apellidos) && d.Apellidos.Contains(busqueda, StringComparison.OrdinalIgnoreCase)) ||
            (!string.IsNullOrEmpty(d.RazonSocial) && d.RazonSocial.Contains(busqueda, StringComparison.OrdinalIgnoreCase))
            ))
        {
            var collapseId = $"upd{denuncia.Id_Denuncia}";
            <div class="card mb-2 collapse-card update-card">
                <div class="card-header d-flex justify-content-between align-items-center"
                     data-bs-toggle="collapse"
                     data-bs-target="#@collapseId"
                     aria-expanded="false"
                     aria-controls="@collapseId">
                    <div class="d-flex flex-wrap align-items-center gap-2">
                        <h5 class="mb-0">Denuncia ID: @denuncia.Id_Denuncia (Actualización)</h5>
                        @if (!string.IsNullOrWhiteSpace(denuncia.OwnerUsername))
                        {
                            <span class="badge @(denuncia.OwnedByCurrentUser ? "text-bg-secondary" : "text-bg-warning")">
                                Responsable: @denuncia.OwnerUsername
                            </span>
                        }
                        @if (denuncia.OwnerWorkGroups.Count > 0)
                        {
                            <span class="badge text-bg-light">
                                Grupo: @string.Join(" / ", denuncia.OwnerWorkGroups)
                            </span>
                        }
                    </div>

                    <div class="d-flex align-items-center">
                        <div class="text-muted small me-3">
                            @if (denuncia.FechaSubidaAGestiona != DateTime.MinValue)
                            {
                                <span>Procesada: @denuncia.FechaSubidaAGestiona.ToString("dd/MM/yyyy")</span>
                            }
                            else if (denuncia.Fecha != DateTime.MinValue)
                            {
                                <span>Creada: @denuncia.Fecha.ToString("dd/MM/yyyy")</span>
                            }
                        </div>

                        <button type="button"
                                class="btn btn-success btn-sm"
                                @onclick:stopPropagation="true"
                                @onclick="() => OpenEnviarAGestionaModal(denuncia)">
                            Configurar actualizacion expediente
                        </button>
                    </div>
                </div>

                <div id="@collapseId" class="collapse">
                    <div class="card-body">
                        <h5 class="section-heading">Datos Generales</h5>
                        <dl class="row">
                            @if (denuncia.Id_RegistroDenuncia != 0)
                            {
                                <dt class="col-sm-3">ID Registro</dt>
                                <dd class="col-sm-9">@denuncia.Id_RegistroDenuncia</dd>
                            }
                            @if (!string.IsNullOrWhiteSpace(denuncia.ExpedienteGestionaMostrable))
                            {
                                <dt class="col-sm-3">Nº expediente Gestiona</dt>
                                <dd class="col-sm-9">@denuncia.ExpedienteGestionaMostrable</dd>
                            }
                            @if (!string.IsNullOrWhiteSpace(denuncia.Etiqueta))
                            {
                                <dt class="col-sm-3">Etiqueta</dt>
                                <dd class="col-sm-9">@denuncia.Etiqueta</dd>
                            }
                            @if (!string.IsNullOrWhiteSpace(denuncia.Estado))
                            {
                                <dt class="col-sm-3">Estado</dt>
                                <dd class="col-sm-9">@denuncia.Estado</dd>
                            }
                            @if (!string.IsNullOrWhiteSpace(denuncia.Tipo_Denuncia))
                            {
                                <dt class="col-sm-3">Tipo</dt>
                                <dd class="col-sm-9">@denuncia.Tipo_Denuncia</dd>
                            }
                        </dl>

                        @if (IsExternalGestionaUpdate(denuncia))
                        {
                            <div class="alert alert-warning" role="alert">
                                @GetExternalUpdateWarningText()
                            </div>
                        }

                        @if (!string.IsNullOrWhiteSpace(denuncia.NombreResuelto) ||
                             !string.IsNullOrWhiteSpace(denuncia.ApellidosResueltos) ||
                             !string.IsNullOrWhiteSpace(denuncia.RazonSocialResuelta) ||
                             !string.IsNullOrWhiteSpace(denuncia.DocumentoResuelto))
                        {
                            <h5 class="section-heading">Denunciante</h5>
                            <dl class="row">
                                @if (!string.IsNullOrWhiteSpace(denuncia.TipoDenunciante))
                                {
                                    <dt class="col-sm-3">Tipo de denunciante</dt>
                                    <dd class="col-sm-9">@denuncia.TipoDenunciante</dd>
                                }
                                @if (!string.IsNullOrWhiteSpace(denuncia.RazonSocialResuelta))
                                {
                                    <dt class="col-sm-3">Razón social</dt>
                                    <dd class="col-sm-9">@denuncia.RazonSocialResuelta</dd>
                                }
                                @if (!string.IsNullOrWhiteSpace(denuncia.NombreResuelto))
                                {
                                    <dt class="col-sm-3">Nombre</dt>
                                    <dd class="col-sm-9">@denuncia.NombreResuelto</dd>
                                }
                                @if (!string.IsNullOrWhiteSpace(denuncia.PrimerApellidoResuelto))
                                {
                                    <dt class="col-sm-3">1º apellido</dt>
                                    <dd class="col-sm-9">@denuncia.PrimerApellidoResuelto</dd>
                                }
                                @if (!string.IsNullOrWhiteSpace(denuncia.SegundoApellidoResuelto))
                                {
                                    <dt class="col-sm-3">2º apellido</dt>
                                    <dd class="col-sm-9">@denuncia.SegundoApellidoResuelto</dd>
                                }
                                @if (!string.IsNullOrWhiteSpace(denuncia.ApellidosResueltos))
                                {
                                    <dt class="col-sm-3">Apellidos</dt>
                                    <dd class="col-sm-9">@denuncia.ApellidosResueltos</dd>
                                }
                                @if (!string.IsNullOrWhiteSpace(denuncia.DocumentoResuelto))
                                {
                                    <dt class="col-sm-3">@GetDocumentoLabel(denuncia)</dt>
                                    <dd class="col-sm-9">@denuncia.DocumentoResuelto</dd>
                                }
                            </dl>
                        }

                        <h5 class="section-heading">Detalles</h5>
                        <dl class="row">
                            <dt class="col-sm-3">Asunto</dt>
                            <dd class="col-sm-9">@denuncia.Asunto</dd>
                            <dt class="col-sm-3">A Quién</dt>
                            <dd class="col-sm-9">@denuncia.A_Quien_Denuncia</dd>
                            @if (!string.IsNullOrWhiteSpace(denuncia.DenunciadoDetalle))
                            {
                                <dt class="col-sm-3">Detalle denunciado</dt>
                                <dd class="col-sm-9">@denuncia.DenunciadoDetalle</dd>
                            }
                            <dt class="col-sm-3">Descripción</dt>
                            <dd class="col-sm-9">@denuncia.Descripcion_Denuncia</dd>
                            @if (!string.IsNullOrWhiteSpace(denuncia.OrganismoDenunciado))
                            {
                                <dt class="col-sm-3">Organismo previo</dt>
                                <dd class="col-sm-9">@denuncia.OrganismoDenunciado</dd>
                            }
                            @if (!string.IsNullOrWhiteSpace(denuncia.SolicitaProteccion))
                            {
                                <dt class="col-sm-3">Solicita protección</dt>
                                <dd class="col-sm-9">@denuncia.SolicitaProteccion</dd>
                            }
                            @if (!string.IsNullOrWhiteSpace(denuncia.MedidasProteccionSolicitadas))
                            {
                                <dt class="col-sm-3">Medidas solicitadas</dt>
                                <dd class="col-sm-9">@denuncia.MedidasProteccionSolicitadas</dd>
                            }
                            <dt class="col-sm-3">Lugar</dt>
                            <dd class="col-sm-9">@denuncia.Lugar_Hechos</dd>
                            @if (denuncia.Fecha_Hechos != DateTime.MinValue)
                            {
                                <dt class="col-sm-3">Fecha Hechos</dt>
                                <dd class="col-sm-9">@denuncia.Fecha_Hechos.ToString("dd/MM/yyyy")</dd>
                            }
                            @if (!string.IsNullOrWhiteSpace(denuncia.AutorizaRemision))
                            {
                                <dt class="col-sm-3">Autoriza remisión</dt>
                                <dd class="col-sm-9">@denuncia.AutorizaRemision</dd>
                            }
                        </dl>

                        @{
                            var camposFormulario = denuncia.GetCamposFormulario();
                        }
                        @if (camposFormulario.Count > 0)
                        {
                            <h5 class="section-heading">Formulario Original</h5>
                            @foreach (var grupoCampos in camposFormulario.GroupBy(field => string.IsNullOrWhiteSpace(field.Section) ? "Sin sección" : field.Section))
                            {
                                <h6 class="mt-3">@grupoCampos.Key</h6>
                                <dl class="row">
                                    @foreach (var campo in grupoCampos)
                                    {
                                        <dt class="col-sm-4">@campo.Label</dt>
                                        <dd class="col-sm-8">@(string.IsNullOrWhiteSpace(campo.Value) ? "—" : campo.Value)</dd>
                                    }
                                </dl>
                            }
                        }

                        @if (ficherosAdjuntos.TryGetValue(denuncia.Id_Denuncia, out var fAdj) && fAdj.Any())
                        {
                            <h5 class="section-heading">Ficheros pendientes de subir</h5>
                            <table class="table table-striped">
                                <thead>
                                    <tr>
                                        <th>Subir</th>
                                        <th>Nombre</th>
                                        <th>Fecha</th>
                                        <th>Motivo</th>
                                        <th>Tamaño (bytes)</th>
                                        <th>Ver</th>
                                    </tr>
                                </thead>
                                <tbody>
                                    @foreach (var f in fAdj)
                                    {
                                        var isReport = IsReportFileName(f.NombreFichero);
                                        <tr>
                                            <td>
                                                <input class="form-check-input"
                                                       type="checkbox"
                                                       checked="@IsFileSelectedForUpload(denuncia.Id_Denuncia, f.NombreFichero)"
                                                       disabled="@isReport"
                                                       title='@(isReport ? "El report se sube siempre" : "Incluir este fichero en la subida")'
                                                       @onchange="args => ToggleFileSelection(denuncia.Id_Denuncia, f.NombreFichero, args.Value is bool selected && selected)" />
                                            </td>
                                            <td>
                                                @f.NombreFichero
                                                @if (isReport)
                                                {
                                                    <span class="badge bg-primary ms-2">Obligatorio</span>
                                                }
                                            </td>
                                            <td>@FormatFileDate(f.Fecha)</td>
                                            <td>
                                                @if (f.EsReport)
                                                {
                                                    <span class="badge bg-primary">Siempre se sube para notificar</span>
                                                }
                                                else if (IsExternalGestionaUpdate(denuncia))
                                                {
                                                    <span class="badge bg-warning text-dark">@GetExternalUpdateFileReason()</span>
                                                }
                                                else
                                                {
                                                    <span class="badge bg-warning text-dark">Hash nuevo pendiente</span>
                                                }
                                            </td>
                                            <td>@f.Fichero?.Length</td>
                                            <td>
                                                @if (f.Fichero != null && f.Fichero.Length > 0)
                                                {
                                                    <a class="btn btn-primary btn-sm" href="@BuildAttachmentContentUrl(denuncia.Id_Denuncia, f.NombreFichero)" target="_blank" rel="noopener">
                                                        <i class="bi bi-eye"></i> Ver
                                                    </a>
                                                }
                                                else
                                                {
                                                    <span class="text-muted">—</span>
                                                }
                                            </td>
                                        </tr>
                                    }
                                </tbody>
                            </table>
                        }
                        else
                        {
                            <div class="alert alert-light">Sin ficheros pendientes.</div>
                        }
                    </div>
                </div>
            </div>
        }
    </div>
}

<script>
    function createObjectUrl(base64, contentType) {
        const binaryString = window.atob(base64);
        const len = binaryString.length;
        const bytes = new Uint8Array(len);
        for (let i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); }
        const blob = new Blob([bytes], { type: contentType });
        return URL.createObjectURL(blob);
    }
    function openFile(event, base64, contentType) {
        event.preventDefault();
        event.stopPropagation();
        const objectUrl = createObjectUrl(base64, contentType);
        window.open(objectUrl, '_blank');
    }
</script>

@if (showModal && selectedDenuncias != null)
{
    <div class="modal custom-modal fade show update-modal" style="display:block;" tabindex="-1">
        <div class="modal-dialog custom-modal-dialog">
            <div class="modal-content custom-modal-content">
                <div class="modal-header custom-modal-header">
                    <h5 class="modal-title">Enviar a Gestiona</h5>
                    <button type="button" class="btn-close" @onclick="CloseModal"></button>
                </div>

                <div class="modal-body custom-modal-body">
                    @if (autoSearchLoading)
                    {
                        <div class="alert alert-info d-flex align-items-center" role="alert">
                            <div class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></div>
                            Buscando expediente existente en Gestiona…
                        </div>
                    }
                    else if (autoSearchTried && !string.IsNullOrWhiteSpace(autoFoundFileUrl))
                    {
                        <div class="alert alert-success" role="alert">
                            <div class="fw-semibold">Expediente detectado en Gestiona.</div>
                            <div>Asunto: <strong>@(autoFoundTitle ?? "(sin título)")</strong></div>
                            <div class="text-muted small">@autoFoundFileUrl</div>
                            <div class="form-check mt-2">
                                <input class="form-check-input" type="checkbox" id="chkUsarDetectado" @bind="useAutoFoundExpediente" />
                                <label class="form-check-label" for="chkUsarDetectado">
                                    Añadir documentos a este expediente.
                                </label>
                            </div>
                        </div>
                    }
                    else if (autoSearchTried && string.IsNullOrWhiteSpace(autoFoundFileUrl) && string.IsNullOrWhiteSpace(selectedDenuncias!.Expediente_Gestiona))
                    {
                        <div class="alert alert-warning" role="alert">
                            <div class="fw-semibold">No se ha detectado expediente en Gestiona por asunto.</div>
                            <div class="small">Puede que esto no sea una actualización del mismo caso.</div>
                        </div>
                    }

                    @if (!string.IsNullOrWhiteSpace(operationError))
                    {
                        <div class="alert alert-danger" role="alert">
                            @operationError
                        </div>
                    }

                    @if (IsExternalGestionaUpdate(selectedDenuncias))
                    {
                        <div class="alert alert-warning" role="alert">
                            @GetExternalUpdateWarningText()
                        </div>
                    }

                    @if (!string.IsNullOrWhiteSpace(selectedDenuncias.ExpedienteGestionaMostrable))
                    {
                        <div class="alert alert-info" role="alert">
                            <div class="fw-semibold">Expediente de Gestiona donde se hará la actualización</div>
                            <div>Nº expediente: <strong>@selectedDenuncias.ExpedienteGestionaMostrable</strong></div>
                        </div>
                    }

                    <h6 class="modal-section-heading">Descripción</h6>
                    <div class="mb-3">
                        <input type="text" class="form-control" @bind="nuevoAsunto" readonly />
                    </div>

                    <h6 class="modal-section-heading">Nombre de los documentos</h6>
                    <div class="mb-3">
                        <input type="text" class="form-control" @bind="nombreDocumentos"
                               placeholder="Ej.: Gestión AN (Documento Adjunto 1 Gestión AN...)" />
                        <small class="text-muted">
                            Se aplica al modo individual. <em>report.txt</em> se sube como <strong>Denuncia</strong> si entra en esta actualización.
                        </small>
                    </div>

                    <h6 class="modal-section-heading">Modo de subida</h6>
                    <div class="form-check">
                        <input class="form-check-input" type="radio" name="uploadMode" id="modoMerge"
                               checked='@(uploadMode == "merge")' @onclick='() => uploadMode = "merge"' />
                        <label class="form-check-label" for="modoMerge">Unir todos los ficheros en un único PDF</label>
                    </div>
                    <div class="form-check">
                        <input class="form-check-input" type="radio" name="uploadMode" id="modoIndividual"
                               checked='@(uploadMode == "individual")' @onclick='() => uploadMode = "individual"' />
                        <label class="form-check-label" for="modoIndividual">Subir ficheros de forma independiente</label>
                    </div>

                    <h6 class="modal-section-heading">Grupo de destino</h6>
                    <div class="form-check">
                        <input class="form-check-input" type="radio" name="selectedGroup" id="grupo600"
                               checked='@(selectedGroup == "600")' @onclick='() => selectedGroup = "600"' />
                        <label class="form-check-label" for="grupo600">
                            600. Asuntos Jurídicos y Protección a la Persona Denunciante
                        </label>
                    </div>
                    <div class="form-check">
                        <input class="form-check-input" type="radio" name="selectedGroup" id="grupo510"
                               checked='@(selectedGroup == "510")' @onclick='() => selectedGroup = "510"' />
                        <label class="form-check-label" for="grupo510">510. SDI – Investigación Entradas</label>
                    </div>

                    @{
                        var modalThirdParty = selectedThirdParty ?? ThirdPartyIdentityData.FromComplaint(selectedDenuncias);
                    }
                    <h6 class="modal-section-heading mt-3">Tercero (denunciante)</h6>

                    <div class="alert alert-light border mb-3">
                        Los datos del tercero se cargan automáticamente desde la denuncia y no se pueden editar aquí.
                    </div>

                    @if (modalThirdParty.IsAnonymous)
                    {
                        <div class="alert alert-warning mb-3">
                            Denuncia anónima. Se enlazará automáticamente el tercero <strong>00000000T</strong>.
                        </div>
                    }

                    <div class="row g-2">
                        <div class="col-6 mb-2">
                            <label class="form-label">@GetDocumentoLabel(selectedDenuncias)</label>
                            <input class="form-control" value="@GetReadOnlyValue(modalThirdParty.DocumentId)" readonly />
                        </div>
                        <div class="col-6 mb-2">
                            <label class="form-label">Email</label>
                            <input class="form-control" value="@GetReadOnlyValue(modalThirdParty.Email)" readonly />
                        </div>
                    </div>

                    @if (!string.IsNullOrWhiteSpace(selectedDenuncias.TipoDenunciante))
                    {
                        <div class="row g-2">
                            <div class="col-12 mb-2">
                                <label class="form-label">Tipo de denunciante</label>
                                <input class="form-control" value="@selectedDenuncias.TipoDenunciante" readonly />
                            </div>
                        </div>
                    }

                    @if (modalThirdParty.IsLegalEntity)
                    {
                        <div class="row g-2">
                            <div class="col-12 mb-2">
                                <label class="form-label">Razón social</label>
                                <input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.RazonSocialResuelta)" readonly />
                            </div>
                        </div>
                    }
                    else
                    {
                        <div class="row g-2">
                            <div class="col-4 mb-2">
                                <label class="form-label">Nombre</label>
                                <input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.NombreResuelto)" readonly />
                            </div>
                            <div class="col-4 mb-2">
                                <label class="form-label">1º apellido</label>
                                <input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.PrimerApellidoResuelto)" readonly />
                            </div>
                            <div class="col-4 mb-2">
                                <label class="form-label">2º apellido</label>
                                <input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.SegundoApellidoResuelto)" readonly />
                            </div>
                        </div>
                    }

                    @if (!modalThirdParty.IsAnonymous && HasPostalAddress(selectedDenuncias))
                    {
                        <div class="row g-2">
                            <div class="col-12 mb-2">
                                <label class="form-label">Dirección postal</label>
                                <textarea class="form-control" rows="2" readonly>@BuildPostalAddressSummary(selectedDenuncias)</textarea>
                            </div>
                        </div>
                    }

                    <small class="text-muted">
                        Antes de subir la actualización se comprobará el tercero extraído del formulario y, si no está enlazado al expediente, se enlazará.
                    </small>
                </div>

                <div class="modal-footer custom-modal-footer">
                    <button type="button" class="btn btn-secondary" @onclick="CloseModal">Cancelar</button>
                    <button type="button" class="btn btn-primary" disabled="@isUploading" @onclick="ConfirmarEnvio">Confirmar</button>
                </div>
            </div>
        </div>
    </div>
    <div class="modal-backdrop fade show"></div>
}

@code {
    private bool hasLoaded = false;
    private string busqueda = "";
    private List<DenunciasGestiona> actualizaciones = new();
    private Dictionary<int, List<FicherosDenuncias>> ficherosAdjuntos = new();
    private Dictionary<int, HashSet<string>> excludedUploadFiles = new();
    private string loadError = string.Empty;
    private string operationError = string.Empty;
    private string operationNotice = string.Empty;
    private DateOnly? externalUpdateCutoffDate;

    // --- Modal / estado ---
    private bool showModal = false;
    private bool isUploading = false;
    private string uploadMode = "individual";
    private string selectedGroup = "600";
    private string nuevoAsunto = "";
    private string nombreDocumentos = "";
    private DenunciasGestiona? selectedDenuncias;
    private ThirdPartyIdentityData? selectedThirdParty;

    // --- Detección automática en Gestiona por asunto ---
    private bool autoSearchLoading = false;
    private bool autoSearchTried = false;
    private string? autoFoundFileUrl = null;
    private string? autoFoundTitle = null;
    private bool useAutoFoundExpediente = true;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender) return;

        await CargarDatosAsync();
    }

    private async Task CargarDatosAsync()
    {
        try
        {
            loadError = string.Empty;
            var config = await ApiDenuncias.GetAppConfigurationAsync();
            externalUpdateCutoffDate = ParseConfiguredCutoffDate(config.ExternalUpdateCutoffDate);
            var todas = await CargarDenunciasJsonAsync();
            actualizaciones = todas
                .Where(d => d.EsActualizacion)
                .OrderByDescending(d => d.FechaSubidaAGestiona != DateTime.MinValue ? d.FechaSubidaAGestiona : d.Fecha)
                .ToList();

            ficherosAdjuntos = await CargarFicherosPorDenunciaAsync(actualizaciones);
            actualizaciones = actualizaciones
                .Where(d => ficherosAdjuntos.ContainsKey(d.Id_Denuncia))
                .ToList();
        }
        catch (Exception ex)
        {
            actualizaciones.Clear();
            ficherosAdjuntos.Clear();
            loadError = $"No se han podido cargar las actualizaciones: {ex.Message}";
        }

        hasLoaded = true;
        StateHasChanged();
    }

    private async Task<List<DenunciasGestiona>> CargarDenunciasJsonAsync()
    {
        return await DenunciaStore.GetDenunciasByScopeAsync(DenunciaListScope.Updates);
    }

    private async Task<Dictionary<int, List<FicherosDenuncias>>> CargarFicherosPorDenunciaAsync(IEnumerable<DenunciasGestiona> denuncias)
    {
        var result = new Dictionary<int, List<FicherosDenuncias>>();
        foreach (var denuncia in denuncias.Where(d => d.Id_Denuncia > 0).DistinctBy(d => d.Id_Denuncia))
        {
            var ficheros = GetPendingUpdateFilesForComplaint(
                denuncia,
                await DenunciaStore.GetFicherosByDenunciaAsync(denuncia.Id_Denuncia));
            if (ficheros.Count > 0)
            {
                result[denuncia.Id_Denuncia] = ficheros;
            }
        }

        return result;
    }

    private async Task OpenEnviarAGestionaModal(DenunciasGestiona d)
    {
        if (d.RequiresOwnerConfirmation)
        {
            var confirmed = await JSRuntime.InvokeAsync<bool>(
                "confirm",
                $"La denuncia #{d.Id_Denuncia} es propiedad de {d.OwnerUsername}. " +
                "Puedes actualizarla porque pertenece a un usuario de tu grupo. Deseas continuar?");
            if (!confirmed)
            {
                return;
            }
        }

        selectedDenuncias = d;
        nuevoAsunto = string.IsNullOrWhiteSpace(d.NombreDenuncia) ? $"Denuncia-{d.Id_Denuncia}-CD" : d.NombreDenuncia;
        nombreDocumentos = "";
        selectedThirdParty = ThirdPartyIdentityData.FromComplaint(d);
        selectedGroup = NormalizeUpdateGroup(selectedGroup);
        operationError = string.Empty;
        operationNotice = string.Empty;

        autoFoundFileUrl = null;
        autoFoundTitle = null;
        useAutoFoundExpediente = true;
        autoSearchTried = false;

        if (!string.IsNullOrWhiteSpace(d.Expediente_Gestiona) &&
            string.IsNullOrWhiteSpace(d.CodigoExpedienteGestiona))
        {
            await SincronizarExpedienteGestionaAsync(d, d.Expediente_Gestiona);
        }

        if (string.IsNullOrWhiteSpace(d.Expediente_Gestiona))
        {
            autoSearchLoading = true;
            showModal = true;
            StateHasChanged();

            try
            {
                // Búsqueda automática desactivada temporalmente
            }
            catch
            {
                // no bloquea
            }
            finally
            {
                autoSearchLoading = false;
                autoSearchTried = true;
                StateHasChanged();
            }
        }
        else
        {
            autoFoundFileUrl = null;
            autoFoundTitle = null;
            autoSearchTried = false;
            useAutoFoundExpediente = true;
            showModal = true;
            StateHasChanged();
        }
    }

    private void CloseModal()
    {
        showModal = false;
        selectedDenuncias = null;
        selectedThirdParty = null;
        nuevoAsunto = "";
        nombreDocumentos = "";
        operationError = string.Empty;
        autoFoundFileUrl = null;
        autoFoundTitle = null;
        autoSearchLoading = false;
        autoSearchTried = false;
        useAutoFoundExpediente = true;
    }

    private static bool IsReportFileName(string? fileName)
    {
        if (string.IsNullOrWhiteSpace(fileName))
        {
            return false;
        }

        var name = Path.GetFileNameWithoutExtension(fileName);
        var extension = Path.GetExtension(fileName);
        return name.StartsWith("report", StringComparison.OrdinalIgnoreCase) &&
               (extension.Equals(".txt", StringComparison.OrdinalIgnoreCase) ||
                extension.Equals(".pdf", StringComparison.OrdinalIgnoreCase));
    }
    private string FixFileName(string input)
    {
        var n = input.Normalize(NormalizationForm.FormD);
        var sb = new StringBuilder();
        foreach (var c in n)
            if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark) sb.Append(c);
        var clean = sb.ToString().Normalize(NormalizationForm.FormC).Replace(' ', '_');
        return new string(clean.Where(ch => ch <= 127).ToArray());
    }

    private string BuildMergedAttachmentsFileName(int denunciaId, DateTime timestampUtc)
    {
        var baseName = string.IsNullOrWhiteSpace(nombreDocumentos)
            ? $"Adjuntos {denunciaId}_{timestampUtc:yyyyMMddHHmmss}"
            : nombreDocumentos.Trim();

        return FixFileName($"{Path.GetFileNameWithoutExtension(baseName)}.pdf");
    }

    private async Task ActualizarDenunciaAsync(DenunciasGestiona d)
    {
        await DenunciaStore.UpsertDenunciaAsync(d);
    }

    private async Task ConfirmarEnvio()
    {
        if (selectedDenuncias == null) return;

        if (!selectedDenuncias.EsActualizacion)
        {
            CloseModal();
            Navigation.NavigateTo("/Pendientes");
            return;
        }

        try
        {
            isUploading = true;
            operationError = string.Empty;
            operationNotice = string.Empty;
            selectedGroup = NormalizeUpdateGroup(selectedGroup);
            using var busy = Busy.Show(
                "Enviando actualizacion",
                "Preparando expediente, carpeta de actualizacion y documentos.");
            StateHasChanged();
            await Task.Yield();

            // 1) Ficheros a subir
            Busy.Update(message: "Cargando ficheros pendientes de esta actualizacion.", detail: "Paso 1 de 8");
            var existentesF = await DenunciaStore.GetFicherosByDenunciaAsync(selectedDenuncias.Id_Denuncia);
            var fDenuncia = GetPendingUpdateFilesForComplaint(selectedDenuncias, existentesF);
            var ficherosNoSeleccionados = GetExcludedUploadFileNames(selectedDenuncias.Id_Denuncia, fDenuncia);
            var ficherosSeleccionados = GetSelectedUploadFiles(selectedDenuncias.Id_Denuncia, fDenuncia);

            var todos = new List<(string FileName, byte[] Content)>();
            foreach (var f in ficherosSeleccionados)
            {
                if (f.Fichero == null) continue;
                todos.Add((f.NombreFichero ?? string.Empty, f.Fichero));
            }

            var ficherosVacios = todos
                .Where(t => t.Content.Length == 0)
                .Select(t => string.IsNullOrWhiteSpace(t.FileName) ? "(sin nombre)" : t.FileName)
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList();
            todos = todos
                .Where(t => t.Content.Length > 0)
                .ToList();

            if (!todos.Any())
            {
                operationError = ficherosVacios.Count == 0
                    ? $"La denuncia #{selectedDenuncias.Id_Denuncia} no tiene ficheros pendientes para subir como actualización."
                    : $"La denuncia #{selectedDenuncias.Id_Denuncia} solo tiene ficheros vacíos y no se ha subido nada. Ficheros omitidos: {string.Join(", ", ficherosVacios)}.";
                return;
            }

            // 2) Determinar expediente destino
            var expedienteCreadoEnGestiona = false;
            string fileUrl;
            if (selectedDenuncias.EnGestiona && !string.IsNullOrWhiteSpace(selectedDenuncias.Expediente_Gestiona))
            {
                fileUrl = selectedDenuncias.Expediente_Gestiona!;
            }
            else if (!string.IsNullOrWhiteSpace(autoFoundFileUrl) && useAutoFoundExpediente)
            {
                fileUrl = autoFoundFileUrl!;
                selectedDenuncias.Expediente_Gestiona = fileUrl;
                selectedDenuncias.EnGestiona = true;
            }
            else
            {
                Busy.Update(message: "Creando expediente nuevo en Gestiona.", detail: "Paso 2 de 8");
                var createdFile = await ApiDenuncias.CreateGestionaFileAsync(
                    nuevoAsunto,
                    "RQ2ZLC - Expediente de Denuncias",
                    "3109963"
                );
                fileUrl = createdFile.FileUrl;
                Busy.Update(message: "Abriendo expediente y asignando el grupo elegido.", detail: "Paso 2 de 8");
                await ApiDenuncias.OpenGestionaFileAsync(
                    fileUrl,
                    createdFile.FileOpenUrl,
                    assignedGroupCode: selectedGroup,
                    confidential: selectedDenuncias.Confidencial,
                    freeTitle: nuevoAsunto
                );
                selectedDenuncias.Expediente_Gestiona = fileUrl;
                selectedDenuncias.EnGestiona = true;
                expedienteCreadoEnGestiona = true;
            }

            Busy.Update(message: "Guardando referencia local del expediente.", detail: "Paso 3 de 8");
            selectedDenuncias.Expediente_Gestiona = fileUrl;
            selectedDenuncias.EnGestiona = true;

            Busy.Update(message: "Asignando el expediente al grupo elegido.", detail: "Paso 3 de 8");
            await ApiDenuncias.AssignGestionaFileAsync(fileUrl, selectedGroup);

            var thirdParty = selectedThirdParty ?? ThirdPartyIdentityData.FromComplaint(selectedDenuncias);

            Busy.Update(message: "Resolviendo tercero y enlazandolo al expediente.", detail: "Paso 4 de 8");
            var thirdResult = await ApiDenuncias.EnsureGestionaThirdAndLinkAsync(fileUrl, thirdParty);

            var ahoraUtc = DateTime.UtcNow;
            var carpetaActualizacion = FixFileName($"Actualizacion {DateTime.Now:yyyy-MM-dd HH-mm}");
            Busy.Update(message: "Creando carpeta de actualizacion en Gestiona.", detail: "Paso 5 de 8");
            var carpetaActualizacionGestiona = await ApiDenuncias.CreateGestionaFolderAsync(fileUrl, carpetaActualizacion);
            var documentsTargetUrl = carpetaActualizacionGestiona.DocumentsTargetUrl;
            var nombresOriginalesSubidos = new List<string>();
            var nombresFinalesSubidos = new List<string>();
            string? documentoParaTramitar = null;

            var report = todos.FirstOrDefault(t =>
                IsReportFileName(t.FileName));

            if (!string.IsNullOrWhiteSpace(report.FileName))
            {
                Busy.Update(message: "Preparando y subiendo el documento principal.", detail: "Paso 6 de 8");
                var reportPdfBytes = PdfHelper.MergeReportToPdf(
                    new (string FileName, byte[] Content)[] { (report.FileName, report.Content) });
                var reportFinalName = FixFileName("Denuncia.pdf");
                documentoParaTramitar = await ApiDenuncias.UploadGestionaDocumentAsync(
                    documentsTargetUrl,
                    reportPdfBytes,
                    reportFinalName);
                nombresOriginalesSubidos.Add(report.FileName);
                nombresFinalesSubidos.Add($"{carpetaActualizacion}/{reportFinalName}");
            }

            var adjuntos = todos
                .Where(t => !IsReportFileName(t.FileName))
                .ToList();

            if (adjuntos.Count > 0 && uploadMode == "merge")
            {
                Busy.Update(message: "Uniendo adjuntos nuevos en un unico PDF y subiendolo.", detail: "Paso 7 de 8");
                var pdfBytes = PdfHelper.MergeFilesToPdf(adjuntos);
                var pdfName = BuildMergedAttachmentsFileName(selectedDenuncias.Id_Denuncia, ahoraUtc);
                var docUrl = await ApiDenuncias.UploadGestionaDocumentAsync(documentsTargetUrl, pdfBytes, pdfName);
                if (string.IsNullOrWhiteSpace(documentoParaTramitar))
                {
                    documentoParaTramitar = docUrl;
                }

                foreach (var adjunto in adjuntos)
                {
                    nombresOriginalesSubidos.Add(adjunto.FileName);
                }

                nombresFinalesSubidos.Add($"{carpetaActualizacion}/{pdfName}");
            }
            else
            {
                int i = 1;
                string baseNombre = string.IsNullOrWhiteSpace(nombreDocumentos) ? "" : $" {nombreDocumentos}";

                foreach (var t in adjuntos)
                {
                    Busy.Update(
                        message: $"Subiendo adjunto nuevo {i} de {adjuntos.Count}.",
                        detail: "Paso 7 de 8",
                        current: i,
                        total: adjuntos.Count);

                    var origName = t.FileName;
                    var content = t.Content;
                    var ext = Path.GetExtension(origName).ToLowerInvariant();

                    byte[] bytesParaSubir = content;
                    string finalName;

                    if (ext == ".txt")
                    {
                        bytesParaSubir = PdfHelper.MergeFilesToPdf(
                            new (string FileName, byte[] Content)[] { (origName, content) });
                        finalName = FixFileName($"Documento Adjunto {i}{baseNombre}.pdf");
                    }
                    else
                    {
                        finalName = FixFileName($"Documento Adjunto {i}{baseNombre}{ext}");
                    }

                    var docUrl = await ApiDenuncias.UploadGestionaDocumentAsync(documentsTargetUrl, bytesParaSubir, finalName);
                    if (string.IsNullOrWhiteSpace(documentoParaTramitar))
                    {
                        documentoParaTramitar = docUrl;
                    }

                    nombresOriginalesSubidos.Add(origName);
                    nombresFinalesSubidos.Add($"{carpetaActualizacion}/{finalName}");
                    i++;
                }
            }

            selectedDenuncias.ArchivoElegido = string.Join(";", nombresFinalesSubidos);

            if (!string.IsNullOrWhiteSpace(documentoParaTramitar))
            {
                Busy.Update(message: "Finalizando el documento principal en Gestiona.", detail: "Paso 8 de 8");
                selectedGroup = NormalizeUpdateGroup(selectedGroup);
                await ApiDenuncias.TramitarGestionaDocumentAsync(
                    documentoParaTramitar,
                    selectedGroup,
                    selectedDenuncias.Id_Denuncia,
                    isUpdate: true);
            }

            foreach (var orig in nombresOriginalesSubidos)
            {
                var f = existentesF.FirstOrDefault(x => x.NombreFichero == orig);
                if (f != null)
                {
                    f.Subido = true;
                    f.FechaSubida = ahoraUtc;
                }
            }

            Busy.Update(message: "Actualizando trazabilidad en la base de datos.", detail: "Finalizando");
            await DenunciaStore.MarkFicherosAsUploadedAsync(
                selectedDenuncias.Id_Denuncia,
                nombresOriginalesSubidos,
                ahoraUtc);

            selectedDenuncias.EsActualizacion = false;
            selectedDenuncias.NombreDenuncia = nuevoAsunto;
            selectedDenuncias.FechaSubidaAGestiona = ahoraUtc;
            selectedDenuncias.UltimaSubidaGestionaTipo = "Actualización";
            selectedDenuncias.UltimoGrupoAsignadoGestiona = GetGestionaGroupDisplay(selectedGroup);
            await SincronizarExpedienteGestionaAsync(selectedDenuncias, fileUrl);
            await ActualizarDenunciaAsync(selectedDenuncias);
            var historialAviso = await RegistrarHistorialGestionaAsync(
                selectedDenuncias,
                "Actualización",
                selectedGroup,
                ahoraUtc,
                string.Join("; ", nombresFinalesSubidos));

            var denunciaProcesadaId = selectedDenuncias.Id_Denuncia;
            actualizaciones.RemoveAll(x => x.Id_Denuncia == denunciaProcesadaId);
            excludedUploadFiles.Remove(denunciaProcesadaId);
            CloseModal();

            var avisos = new List<string>();
            if (ficherosVacios.Count > 0)
            {
                avisos.Add($"Se omitieron ficheros vacíos: {string.Join(", ", ficherosVacios)}.");
            }
            if (ficherosNoSeleccionados.Count > 0)
            {
                avisos.Add($"No se subieron por selección del usuario: {string.Join(", ", ficherosNoSeleccionados)}.");
            }
            if (thirdResult.Warnings.Count > 0)
            {
                avisos.AddRange(thirdResult.Warnings);
            }
            if (!string.IsNullOrWhiteSpace(historialAviso))
            {
                avisos.Add(historialAviso);
            }
            var expedienteInfo = string.IsNullOrWhiteSpace(selectedDenuncias.ExpedienteGestionaMostrable)
                ? string.Empty
                : $" Nº expediente Gestiona: {selectedDenuncias.ExpedienteGestionaMostrable}.";
            var resumen = expedienteCreadoEnGestiona
                ? $"Actualización #{denunciaProcesadaId}: se ha creado el expediente en Gestiona y se han subido los documentos.{expedienteInfo}"
                : $"Actualización #{denunciaProcesadaId}: se han añadido los documentos al expediente de Gestiona.{expedienteInfo}";
            operationNotice = avisos.Count > 0
                ? $"{resumen} {string.Join(" ", avisos)}"
                : resumen;
            StateHasChanged();
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine($"Error al confirmar envío (Actualizaciones): {ex}");
            operationError = $"No se ha podido completar la actualización #{selectedDenuncias?.Id_Denuncia}: {ex.Message}";
        }
        finally
        {
            isUploading = false;
            StateHasChanged();
        }
    }

    private bool IsFileSelectedForUpload(int denunciaId, string? fileName)
    {
        if (IsReportFileName(fileName))
        {
            return true;
        }

        return string.IsNullOrWhiteSpace(fileName) ||
               !excludedUploadFiles.TryGetValue(denunciaId, out var excluded) ||
               !excluded.Contains(fileName);
    }

    private void ToggleFileSelection(int denunciaId, string? fileName, bool selected)
    {
        if (string.IsNullOrWhiteSpace(fileName) || IsReportFileName(fileName))
        {
            return;
        }

        if (!excludedUploadFiles.TryGetValue(denunciaId, out var excluded))
        {
            excluded = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            excludedUploadFiles[denunciaId] = excluded;
        }

        if (selected)
        {
            excluded.Remove(fileName);
            if (excluded.Count == 0)
            {
                excludedUploadFiles.Remove(denunciaId);
            }
        }
        else
        {
            excluded.Add(fileName);
        }
    }

    private List<FicherosDenuncias> GetSelectedUploadFiles(int denunciaId, IEnumerable<FicherosDenuncias> files)
    {
        return files
            .Where(file => IsReportFileName(file.NombreFichero) || IsFileSelectedForUpload(denunciaId, file.NombreFichero))
            .ToList();
    }

    private List<string> GetExcludedUploadFileNames(int denunciaId, IEnumerable<FicherosDenuncias> files)
    {
        return files
            .Where(file => !IsReportFileName(file.NombreFichero) && !IsFileSelectedForUpload(denunciaId, file.NombreFichero))
            .Select(file => string.IsNullOrWhiteSpace(file.NombreFichero) ? "(sin nombre)" : file.NombreFichero)
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
            .ToList();
    }
    private static string BuildAttachmentContentUrl(int denunciaId, string? fileName)
    {
        return $"/api/denuncias/{denunciaId}/ficheros/content?fileName={Uri.EscapeDataString(fileName ?? string.Empty)}";
    }

    private static string FormatFileDate(DateTime date)
        => date == DateTime.MinValue
            ? "-"
            : date.ToLocalTime().ToString("dd/MM/yyyy HH:mm", CultureInfo.InvariantCulture);

    private string GetContentType(string fileName)
    {
        return Path.GetExtension(fileName).ToLowerInvariant() switch
        {
            ".jpg" or ".jpeg" => "image/jpeg",
            ".png" => "image/png",
            ".gif" => "image/gif",
            ".pdf" => "application/pdf",
            ".txt" => "text/plain",
            _ => "application/octet-stream",
        };
    }

    private static string GetDocumentoLabel(DenunciasGestiona denuncia)
    {
        return string.IsNullOrWhiteSpace(denuncia.TipoDocumentoIdentificativo)
            ? "Documento identificativo"
            : denuncia.TipoDocumentoIdentificativo;
    }

    private static string GetReadOnlyValue(string? value) =>
        string.IsNullOrWhiteSpace(value) ? "—" : value;

    private static bool IsExternalGestionaUpdate(DenunciasGestiona denuncia)
    {
        return denuncia.EsActualizacion &&
               denuncia.EnGestiona &&
               denuncia.FechaSubidaAGestiona == DateTime.MinValue;
    }

    private static bool HasPostalAddress(DenunciasGestiona denuncia)
    {
        return !string.IsNullOrWhiteSpace(denuncia.Direccion) ||
               !string.IsNullOrWhiteSpace(denuncia.DireccionNumero) ||
               !string.IsNullOrWhiteSpace(denuncia.Municipio) ||
               !string.IsNullOrWhiteSpace(denuncia.Provincia) ||
               !string.IsNullOrWhiteSpace(denuncia.CodigoPostal);
    }

    private static string BuildPostalAddressSummary(DenunciasGestiona denuncia)
    {
        var parts = new List<string>();

        var street = string.Join(
            ' ',
            new[]
            {
                denuncia.DireccionTipoVia,
                denuncia.Direccion,
                denuncia.DireccionNumero
            }.Where(value => !string.IsNullOrWhiteSpace(value)));
        if (!string.IsNullOrWhiteSpace(street))
        {
            parts.Add(street);
        }

        var extras = string.Join(
            ", ",
            new[]
            {
                string.IsNullOrWhiteSpace(denuncia.DireccionBloque) ? null : $"Bloque {denuncia.DireccionBloque}",
                string.IsNullOrWhiteSpace(denuncia.DireccionEscalera) ? null : $"Esc. {denuncia.DireccionEscalera}",
                string.IsNullOrWhiteSpace(denuncia.DireccionPiso) ? null : $"Planta {denuncia.DireccionPiso}",
                string.IsNullOrWhiteSpace(denuncia.DireccionPuerta) ? null : $"Puerta {denuncia.DireccionPuerta}",
                denuncia.DireccionExtra
            }.Where(value => !string.IsNullOrWhiteSpace(value)));
        if (!string.IsNullOrWhiteSpace(extras))
        {
            parts.Add(extras);
        }

        var location = string.Join(
            ", ",
            new[]
            {
                denuncia.CodigoPostal,
                denuncia.Municipio,
                denuncia.Provincia
            }.Where(value => !string.IsNullOrWhiteSpace(value)));
        if (!string.IsNullOrWhiteSpace(location))
        {
            parts.Add(location);
        }

        return string.Join(" | ", parts);
    }

    private List<FicherosDenuncias> GetPendingUpdateFilesForComplaint(DenunciasGestiona denuncia, List<FicherosDenuncias> files)
    {
        var cutoffDate = IsExternalGestionaUpdate(denuncia)
            ? externalUpdateCutoffDate
            : null;

        return GetPendingUpdateFiles(files, cutoffDate);
    }

    private static List<FicherosDenuncias> GetPendingUpdateFiles(List<FicherosDenuncias> files, DateOnly? externalUpdateCutoffDate = null)
    {
        var uploadedHashes = files
            .Where(file => file.Subido && !string.IsNullOrWhiteSpace(file.ContentSha256))
            .Select(file => file.ContentSha256)
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        var plannedHashes = new HashSet<string>(uploadedHashes, StringComparer.OrdinalIgnoreCase);
        var pending = new List<FicherosDenuncias>();

        var report = files
            .Where(file => file.EsReport)
            .OrderByDescending(file => file.Fecha)
            .FirstOrDefault();
        if (report is not null && !report.Subido)
        {
            if (string.IsNullOrWhiteSpace(report.ContentSha256) || plannedHashes.Add(report.ContentSha256))
            {
                pending.Add(report);
            }
        }

        foreach (var file in files
                     .Where(file => !file.EsReport && !file.Subido)
                     .OrderBy(file => file.NombreFichero, StringComparer.OrdinalIgnoreCase))
        {
            if (externalUpdateCutoffDate.HasValue && !IsFileAfterCutoff(file, externalUpdateCutoffDate.Value))
            {
                continue;
            }

            if (string.IsNullOrWhiteSpace(file.ContentSha256))
            {
                pending.Add(file);
                continue;
            }

            if (plannedHashes.Add(file.ContentSha256))
            {
                pending.Add(file);
            }
        }

        return pending;
    }

    private static bool IsFileAfterCutoff(FicherosDenuncias file, DateOnly cutoffDate)
    {
        if (file.Fecha == DateTime.MinValue)
        {
            return true;
        }

        return DateOnly.FromDateTime(file.Fecha) > cutoffDate;
    }

    private static DateOnly? ParseConfiguredCutoffDate(string? value)
    {
        return DateOnly.TryParseExact(
            value,
            "yyyy-MM-dd",
            CultureInfo.InvariantCulture,
            DateTimeStyles.None,
            out var parsed)
            ? parsed
            : null;
    }

    private string GetExternalUpdateWarningText()
    {
        const string prefix = "Esta actualizacion corresponde a una denuncia que ya existia en Gestiona, pero no consta como subida desde esta app.";
        return externalUpdateCutoffDate.HasValue
            ? $"{prefix} Como no tenemos hashes previos, solo se propondran como nuevos los adjuntos con fecha posterior a {externalUpdateCutoffDate.Value:dd/MM/yyyy}."
            : $"{prefix} Configura una fecha de corte para que los adjuntos antiguos no aparezcan como nuevos.";
    }

    private string GetExternalUpdateFileReason()
    {
        return externalUpdateCutoffDate.HasValue
            ? $"Posterior al corte {externalUpdateCutoffDate.Value:dd/MM/yyyy}"
            : "Sin historico local";
    }

    private static string NormalizeUpdateGroup(string? groupCode)
        => groupCode == "510" ? "510" : "600";

    private static string GetGestionaGroupDisplay(string? groupCode)
    {
        return string.IsNullOrWhiteSpace(groupCode) ? string.Empty : groupCode.Trim();
    }

    private async Task<string?> RegistrarHistorialGestionaAsync(
        DenunciasGestiona denuncia,
        string tipoSubida,
        string grupo,
        DateTime uploadedAtUtc,
        string documentos)
    {
        try
        {
            await ApiDenuncias.RegisterGestionaUploadHistoryAsync(
                new GestionaUploadHistoryCreateRequest(
                    denuncia.Id_Denuncia,
                    denuncia.Expediente_Gestiona ?? string.Empty,
                    denuncia.ExpedienteGestionaMostrable,
                    tipoSubida,
                    grupo,
                    uploadedAtUtc,
                    denuncia.NombreDenuncia ?? string.Empty,
                    documentos));
            return null;
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine($"No se ha podido registrar el historico de subida a Gestiona: {ex}");
            return "La subida se ha completado, pero no se ha podido registrar el histórico operativo en la aplicación.";
        }
    }

    private async Task SincronizarExpedienteGestionaAsync(DenunciasGestiona denuncia, string fileUrl)
    {
        if (string.IsNullOrWhiteSpace(fileUrl))
        {
            return;
        }

        try
        {
            var expediente = await ApiDenuncias.GetGestionaExpedienteAsync(fileUrl);
            if (expediente is null)
            {
                return;
            }

            denuncia.Expediente_Gestiona = expediente.FileUrl;
            if (!string.IsNullOrWhiteSpace(expediente.CodigoExpediente))
            {
                denuncia.CodigoExpedienteGestiona = expediente.CodigoExpediente;
            }

            if (!string.IsNullOrWhiteSpace(expediente.FreeTitle))
            {
                denuncia.NombreDenuncia = expediente.FreeTitle;
            }
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine($"No se ha podido sincronizar el expediente de Gestiona: {ex}");
        }
    }

}

