@page "/GestionZip"
@rendermode @(new InteractiveServerRenderMode(prerender: false))
@attribute [Authorize]
@implements IAsyncDisposable
@using System.Globalization
@using GestionaDenunciasAN.Models
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject ApiDenunciasClient ApiDenuncias
@inject IJSRuntime JSRuntime
@inject UiBusyService Busy

<PageTitle>Entrada de denuncias</PageTitle>

<style>
    .report-detail-overlay {
        position: fixed;
        inset: 0;
        z-index: 4500;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: clamp(1rem, 3vw, 2rem);
        background: rgba(6, 22, 41, 0.62);
        overflow: hidden;
    }

    .report-detail-dialog {
        width: min(820px, 100%);
        max-height: min(88vh, 860px);
        display: flex;
    }

    .report-detail-card {
        width: 100%;
        display: flex;
        flex-direction: column;
        overflow: hidden;
        border: 1px solid rgba(215, 228, 241, 0.92);
        border-radius: 22px;
        background: #ffffff;
        box-shadow: 0 28px 90px rgba(6, 22, 41, 0.38);
        color: var(--app-ink, #12395f);
    }

    .report-detail-header,
    .report-detail-footer {
        flex: 0 0 auto;
        background: #ffffff;
    }

    .report-detail-header {
        display: flex;
        justify-content: space-between;
        gap: 1rem;
        padding: 1.25rem 1.35rem 1rem;
        border-bottom: 1px solid var(--app-border, #d7e4f1);
    }

    .report-detail-body {
        flex: 1 1 auto;
        overflow-y: auto;
        padding: 1rem 1.35rem;
        background: #ffffff;
    }

    .report-detail-footer {
        display: flex;
        justify-content: flex-end;
        padding: 1rem 1.35rem;
        border-top: 1px solid var(--app-border, #d7e4f1);
    }

    .report-detail-close {
        flex: 0 0 auto;
    }
</style>

<div class="container py-4">
    <div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-4">
        <div>
            <h3 class="mb-1">Entrada de denuncias</h3>
            <p class="text-muted mb-0">
                La app mantiene su propia sesion activa. Cuando caduque GlobalLeaks, aqui solo hara falta renovar el 2FA
                para seguir trayendo denuncias.
            </p>
        </div>
    </div>

    @if (!string.IsNullOrWhiteSpace(StatusMessage))
    {
        <div class="alert @StatusCss mb-4">@StatusMessage</div>
    }

    @if (!string.IsNullOrWhiteSpace(FilterWarningMessage))
    {
        <div class="alert @FilterWarningCss mb-4">@FilterWarningMessage</div>
    }

    <div class="row g-4">
        <div class="col-lg-4">
            <div class="card shadow-sm h-100">
                <div class="card-body">
                    <h5 class="card-title">Sesion GlobalLeaks</h5>
                    <p class="mb-2"><strong>Usuario:</strong> @CurrentUsername</p>
                    <p class="mb-2"><strong>Estado:</strong> @SessionStatusText</p>
                    <p class="mb-2">
                        <strong>Ultima descarga registrada:</strong>
                        @(UserInboxState.LastDownloadedReportMomentUtc is null
                            ? "Sin descargas previas"
                            : UserInboxState.LastDownloadedReportMomentUtc.Value.ToLocalTime().ToString("dd/MM/yyyy HH:mm"))
                    </p>
                    <p class="text-muted small mb-4">
                        La base de datos lleva la cuenta de lo que ya ha descargado este usuario, de lo que han descargado otros
                        y de si el expediente ya esta creado en Gestiona.
                    </p>

                    <div class="mb-3">
                        <label class="form-label">Nuevo codigo 2FA</label>
                        <input class="form-control"
                               @bind="RenewAuthcode"
                               maxlength="6"
                               inputmode="numeric"
                               disabled="@(!RenewPrepared || RenewBusy)"
                               placeholder="123456" />
                    </div>

                    <button type="button" class="btn btn-primary w-100" @onclick="RenewSessionAsync" disabled="@RenewBusy">
                        @(RenewBusy ? SessionRenewBusyText : SessionRenewButtonText)
                    </button>
                </div>
            </div>
        </div>

        <div class="col-lg-8">
            <div class="card shadow-sm">
                <div class="card-body">
                    <div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-3">
                        <div>
                            <h5 class="card-title mb-1">Bandeja GlobalLeaks</h5>
                            <p class="text-muted mb-0">
                                Puedes revisar solo lo nuevo, las actualizaciones desde tu ultima descarga o un intervalo de fechas.
                            </p>
                        </div>
                        <button type="button" class="btn btn-outline-primary" @onclick="LoadReportsAsync" disabled="@ReportsBusy || !CanUseGlobalLeaks">
                            @(ReportsBusy ? "Actualizando..." : "Actualizar denuncias")
                        </button>
                    </div>

                    <div class="row g-3 mb-3">
                        <div class="col-md-4">
                            <label class="form-label">Tipo</label>
                            <select class="form-select" value="@Filter" @onchange="OnFilterChanged">
                                <option value="all">Todas</option>
                                <option value="new">Nuevas / sin leer</option>
                                <option value="updated">Actualizaciones</option>
                            </select>
                        </div>

                        <div class="col-md-4">
                            <label class="form-label">Periodo</label>
                            <select class="form-select" value="@DateScope" @onchange="OnDateScopeChanged">
                                <option value="all">Todo el buzon</option>
                                <option value="since_mine">Desde mi ultima descarga</option>
                                <option value="range">Intervalo de fechas</option>
                            </select>
                        </div>

                        <div class="col-md-4">
                            <label class="form-label">Canal</label>
                            <select class="form-select" value="@SelectedChannel" @onchange="OnChannelChanged">
                                <option value="">Todos</option>
                                @foreach (var context in Contexts)
                                {
                                    <option value="@context.Id">@context.Name</option>
                                }
                            </select>
                        </div>

                        <div class="col-md-3">
                            <label class="form-label">Desde</label>
                            <input class="form-control"
                                   type="date"
                                   value="@DateFrom"
                                   disabled="@(DateScope != "range")"
                                   @onchange="OnDateFromChanged" />
                        </div>

                        <div class="col-md-3">
                            <label class="form-label">Hasta</label>
                            <input class="form-control"
                                   type="date"
                                   value="@DateTo"
                                   disabled="@(DateScope != "range")"
                                   @onchange="OnDateToChanged" />
                        </div>

                        <div class="col-md-6">
                            <label class="form-label">Buscar</label>
                            <input class="form-control"
                                   value="@SearchTerm"
                                   @oninput="OnSearchChanged"
                                   placeholder="# denuncia o canal" />
                        </div>
                    </div>

                    <div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-3">
                        <div class="text-muted">
                            @VisibleReports.Count denuncia(s) visibles, @SelectedReportsCount seleccionada(s).
                            @if (UserInboxState.LastDownloadedReportMomentUtc is not null)
                            {
                                <span class="d-block small">
                                    Referencia de ultima descarga: @UserInboxState.LastDownloadedReportMomentUtc.Value.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
                                </span>
                            }
                        </div>
                        <div class="d-flex gap-2">
                            <button type="button" class="btn btn-outline-secondary btn-sm" @onclick="ToggleSelectAll" disabled="@(!VisibleReports.Any())">
                                @SelectAllLabel
                            </button>
                            <button type="button" class="btn btn-success btn-sm" @onclick="ImportSelectedAsync" disabled="@(!CanImportSelected)">
                                @(ImportBusy ? "Importando..." : "Importar seleccionadas")
                            </button>
                        </div>
                    </div>

                    <div class="table-responsive">
                        <table class="table table-hover align-middle">
                            <thead>
                                <tr>
                                    <th style="width: 3rem;"></th>
                                    <th>#</th>
                                    <th>Canal</th>
                                    <th>Presentacion</th>
                                    <th>Ultima actualizacion</th>
                                    <th>Estado</th>
                                    <th>Seguimiento</th>
                                    <th style="width: 7rem;">Detalle</th>
                                </tr>
                            </thead>
                            <tbody>
                                @if (!CanUseGlobalLeaks)
                                {
                                    <tr>
                                        <td colspan="8" class="text-muted">
                                            Renueva la sesion de GlobalLeaks con un 2FA valido para cargar la bandeja.
                                        </td>
                                    </tr>
                                }
                                else if (ReportsBusy)
                                {
                                    <tr>
                                        <td colspan="8" class="text-muted">Cargando denuncias...</td>
                                    </tr>
                                }
                                else if (!VisibleReports.Any())
                                {
                                    <tr>
                                        <td colspan="8" class="text-muted">No hay denuncias con los filtros actuales.</td>
                                    </tr>
                                }
                                else
                                {
                                    @foreach (var report in VisibleReports)
                                    {
                                        <tr @key="report.Id" class="@GetReportRowCss(report)">
                                            <td>
                                                <input type="checkbox"
                                                       checked="@SelectedIds.Contains(report.Id)"
                                                       @onchange="@((ChangeEventArgs args) => ToggleSelection(report.Id, args))" />
                                            </td>
                                            <td><strong>#@(report.Progressive ?? 0)</strong></td>
                                            <td>@(report.ContextName ?? report.ContextId ?? "-")</td>
                                            <td>@FormatDate(report.CreationDate)</td>
                                            <td>@FormatDate(report.UpdateDate)</td>
                                            <td>
                                                <span class="badge @GetStatusBadgeCss(report)">
                                                    @GetStatusLabel(report)
                                                </span>
                                            </td>
                                            <td>
                                                <span class="badge @GetTrackingBadgeCss(report)">
                                                    @GetTrackingLabel(report)
                                                </span>
                                                @if (!string.IsNullOrWhiteSpace(report.TrackingNote))
                                                {
                                                    <div class="small text-muted mt-1">@report.TrackingNote</div>
                                                }
                                            </td>
                                            <td>
                                                <button type="button"
                                                        class="btn btn-outline-secondary btn-sm"
                                                        title="Consulta mensajes y ficheros. GlobalLeaks puede marcar la denuncia como leida."
                                                        @onclick="@(() => OpenReportDetailAsync(report))"
                                                        disabled="@DetailBusy">
                                                    Ver detalle
                                                </button>
                                            </td>
                                        </tr>
                                    }
                                }
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

@if (DetailModalVisible)
{
    <div class="report-detail-overlay" role="dialog" aria-modal="true" aria-labelledby="report-detail-title">
        <div class="report-detail-dialog">
            <div class="report-detail-card">
                <div class="report-detail-header">
                    <div>
                        <h5 id="report-detail-title" class="mb-0">Contenido de denuncia #@(DetailReport?.Progressive ?? 0)</h5>
                        <small class="text-muted">Mensajes y ficheros comparados contra el ultimo acceso registrado.</small>
                    </div>
                    <button type="button" class="btn-close report-detail-close" aria-label="Cerrar" @onclick="CloseReportDetail"></button>
                </div>
                <div class="report-detail-body">
                    <div class="alert alert-warning small">
                        Esta consulta abre el detalle en GlobalLeaks y puede marcar la denuncia como leida.
                    </div>

                    @if (DetailBusy)
                    {
                        <div class="text-muted">Cargando detalle de GlobalLeaks...</div>
                    }
                    else if (!string.IsNullOrWhiteSpace(DetailError))
                    {
                        <div class="alert alert-danger">@DetailError</div>
                    }
                    else if (DetailData is not null)
                    {
                        <p class="small text-muted">
                            Ultimo acceso previo:
                            @(string.IsNullOrWhiteSpace(DetailData.LastAccess) ? "Sin acceso previo" : FormatDate(DetailData.LastAccess)).
                            Los elementos posteriores se marcan como nuevos.
                        </p>

                        <h6 class="text-uppercase text-muted small fw-bold mt-4">Mensajes (@DetailData.Comments.Count)</h6>
                        @if (DetailData.Comments.Count == 0)
                        {
                            <p class="text-muted small">Sin mensajes.</p>
                        }
                        else
                        {
                            @foreach (var comment in DetailData.Comments)
                            {
                                <div class="border rounded p-3 mb-2 @(comment.IsNew ? "border-success bg-success-subtle" : "bg-light")">
                                    <div class="d-flex flex-column flex-md-row justify-content-between gap-2 small text-muted mb-2">
                                        <strong>@GetCommentAuthorLabel(comment.Type)</strong>
                                        <span>@FormatDate(comment.CreationDate)</span>
                                    </div>
                                    @if (comment.IsNew)
                                    {
                                        <span class="badge bg-success mb-2">Nuevo</span>
                                    }
                                    <div style="white-space: pre-wrap;">@(string.IsNullOrWhiteSpace(comment.Content) ? "-" : comment.Content)</div>
                                </div>
                            }
                        }

                        <h6 class="text-uppercase text-muted small fw-bold mt-4">Ficheros del denunciante (@DetailData.WhistleblowerFiles.Count)</h6>
                        @if (DetailData.WhistleblowerFiles.Count == 0)
                        {
                            <p class="text-muted small">Sin ficheros.</p>
                        }
                        else
                        {
                            @foreach (var file in DetailData.WhistleblowerFiles)
                            {
                                <div class="border rounded p-3 mb-2 @(file.IsNew ? "border-success bg-success-subtle" : "bg-light")">
                                    <div class="d-flex flex-column flex-md-row justify-content-between gap-2">
                                        <strong>@(string.IsNullOrWhiteSpace(file.Name) ? "Fichero sin nombre" : file.Name)</strong>
                                        <span class="small text-muted">@FormatDate(file.CreationDate)</span>
                                    </div>
                                    <div class="small text-muted">@FormatBytes(file.Size) @(string.IsNullOrWhiteSpace(file.ContentType) ? string.Empty : $" - {file.ContentType}")</div>
                                    @if (file.IsNew)
                                    {
                                        <span class="badge bg-success mt-2">Nuevo</span>
                                    }
                                </div>
                            }
                        }

                        <h6 class="text-uppercase text-muted small fw-bold mt-4">Ficheros internos/receptor (@DetailData.ReceiverFiles.Count)</h6>
                        @if (DetailData.ReceiverFiles.Count == 0)
                        {
                            <p class="text-muted small">Sin ficheros.</p>
                        }
                        else
                        {
                            @foreach (var file in DetailData.ReceiverFiles)
                            {
                                <div class="border rounded p-3 mb-2 @(file.IsNew ? "border-success bg-success-subtle" : "bg-light")">
                                    <div class="d-flex flex-column flex-md-row justify-content-between gap-2">
                                        <strong>@(string.IsNullOrWhiteSpace(file.Name) ? "Fichero sin nombre" : file.Name)</strong>
                                        <span class="small text-muted">@FormatDate(file.CreationDate)</span>
                                    </div>
                                    <div class="small text-muted">@FormatBytes(file.Size) @(string.IsNullOrWhiteSpace(file.ContentType) ? string.Empty : $" - {file.ContentType}")</div>
                                    @if (file.IsNew)
                                    {
                                        <span class="badge bg-success mt-2">Nuevo</span>
                                    }
                                </div>
                            }
                        }
                    }
                </div>
                <div class="report-detail-footer">
                    <button type="button" class="btn btn-secondary" @onclick="CloseReportDetail">Cerrar</button>
                </div>
            </div>
        </div>
    </div>
}

@code {
    private readonly List<ContextDto> Contexts = [];
    private readonly List<ReportDto> Reports = [];
    private List<ReportDto> VisibleReports = [];
    private HashSet<string> SelectedIds = [];

    private ApiGlobalLeaksSessionDto? SessionInfo;
    private InboxUserState UserInboxState = new();
    private string CurrentUsername { get; set; } = string.Empty;
    private string RenewAuthcode { get; set; } = string.Empty;
    private string RenewPendingLoginId { get; set; } = string.Empty;
    private string Filter { get; set; } = "all";
    private string DateScope { get; set; } = "all";
    private string DateFrom { get; set; } = string.Empty;
    private string DateTo { get; set; } = string.Empty;
    private string SelectedChannel { get; set; } = string.Empty;
    private string SearchTerm { get; set; } = string.Empty;
    private string StatusMessage { get; set; } = string.Empty;
    private string StatusCss { get; set; } = "alert-info";
    private string FilterWarningMessage { get; set; } = string.Empty;
    private string FilterWarningCss { get; set; } = "alert-warning";
    private bool ReportsBusy { get; set; }
    private bool RenewBusy { get; set; }
    private bool ImportBusy { get; set; }
    private bool DetailBusy { get; set; }
    private bool DetailModalVisible { get; set; }
    private string DetailError { get; set; } = string.Empty;
    private ReportDto? DetailReport { get; set; }
    private ReportDetailDto? DetailData { get; set; }
    private readonly Dictionary<string, ReportDetailDto> DetailCache = new(StringComparer.OrdinalIgnoreCase);

    private bool CanUseGlobalLeaks => SessionInfo?.HasActiveSession == true;
    private bool RenewPrepared => !string.IsNullOrWhiteSpace(RenewPendingLoginId);
    private int SelectedReportsCount => SelectedIds.Count;
    private bool CanImportSelected => CanUseGlobalLeaks && SelectedReportsCount > 0 && !ImportBusy;
    private string SessionStatusText => SessionInfo is null
        ? "Sin credenciales guardadas"
        : SessionInfo.HasActiveSession
            ? "Activa"
            : "Pendiente de renovacion 2FA";
    private string SessionRenewButtonText => RenewPrepared
        ? "Validar 2FA y activar"
        : "Preparar renovacion";
    private string SessionRenewBusyText => RenewPrepared
        ? "Validando 2FA..."
        : "Preparando...";
    private string SelectAllLabel => VisibleReports.Count > 0 && VisibleReports.All(report => SelectedIds.Contains(report.Id))
        ? "Deseleccionar todas"
        : "Seleccionar todas";

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
        CurrentUsername = authState.User.Identity?.Name ?? string.Empty;

        await LoadSessionStateAsync();

        if (CanUseGlobalLeaks)
        {
            await LoadReportsAsync();
        }
        else if (SessionInfo is not null)
        {
            SetStatus(
                "La aplicacion sigue iniciada, pero la sesion de GlobalLeaks no esta activa. Introduce un nuevo 2FA para renovarla.",
                "alert-warning");
        }
    }

    private async Task LoadSessionStateAsync()
    {
        SessionInfo = string.IsNullOrWhiteSpace(CurrentUsername)
            ? null
            : await ApiDenuncias.GetGlobalLeaksSessionAsync();

        if (CanUseGlobalLeaks)
        {
            RenewPendingLoginId = string.Empty;
            RenewAuthcode = string.Empty;
        }
    }

    private async Task RenewSessionAsync()
    {
        if (SessionInfo is null)
        {
            SetStatus("No hay credenciales guardadas para este usuario. Cierra sesion y vuelve a entrar.", "alert-danger");
            return;
        }

        if (!RenewPrepared)
        {
            await PrepareRenewSessionAsync();
            return;
        }

        if (string.IsNullOrWhiteSpace(RenewAuthcode) || RenewAuthcode.Trim().Length != 6)
        {
            SetStatus("Introduce un codigo 2FA valido de 6 digitos.", "alert-warning");
            return;
        }

        RenewBusy = true;
        try
        {
            using var busy = Busy.Show(
                "Validando 2FA",
                "Completando la renovacion con el codigo actual de GlobalLeaks.");

            SessionInfo = await ApiDenuncias.RenewGlobalLeaksSessionAsync(
                RenewAuthcode.Trim(),
                RenewPendingLoginId,
                CancellationToken.None);
            RenewAuthcode = string.Empty;
            RenewPendingLoginId = string.Empty;
            Busy.Update(message: "Sesion renovada. Actualizando la bandeja de entrada.");
            await LoadReportsAsync();
            SetStatus("Sesion de GlobalLeaks renovada correctamente.", "alert-success");
        }
        catch (Exception ex)
        {
            SetStatus(ex.Message, "alert-danger");
        }
        finally
        {
            RenewBusy = false;
        }
    }

    private async Task PrepareRenewSessionAsync()
    {
        RenewBusy = true;
        try
        {
            using var busy = Busy.Show(
                "Preparando renovacion",
                "Validando credenciales guardadas y preparando GlobalLeaks antes de pedir el codigo 2FA.");

            var prepared = await ApiDenuncias.PrepareGlobalLeaksSessionRenewalAsync(CancellationToken.None);
            RenewPendingLoginId = prepared.PendingLoginId;
            RenewAuthcode = string.Empty;
            SetStatus("Renovacion preparada. Introduce ahora el codigo 2FA que este activo.", "alert-info");
        }
        catch (Exception ex)
        {
            SetStatus(ex.Message, "alert-danger");
        }
        finally
        {
            RenewBusy = false;
        }
    }

    private async Task LoadReportsAsync()
    {
        if (!CanUseGlobalLeaks)
        {
            return;
        }

        ReportsBusy = true;
        try
        {
            using var busy = Busy.Show(
                "Actualizando bandeja",
                "Consultando GlobalLeaks y cruzando el seguimiento con la base de datos.");

            var inbox = await ApiDenuncias.LoadInboxAsync(CancellationToken.None);

            Contexts.Clear();
            Contexts.AddRange(inbox.Contexts);

            Reports.Clear();
            Reports.AddRange(inbox.Reports);
            DetailCache.Clear();
            UserInboxState = inbox.UserState;
            ApplyFilters();
        }
        catch (UnauthorizedAccessException ex)
        {
            await LoadSessionStateAsync();
            Reports.Clear();
            VisibleReports.Clear();
            SelectedIds.Clear();
            SetStatus(ex.Message, "alert-warning");
        }
        catch (Exception ex)
        {
            SetStatus(ex.Message, "alert-danger");
        }
        finally
        {
            ReportsBusy = false;
        }
    }

    private async Task ImportSelectedAsync()
    {
        if (!CanUseGlobalLeaks)
        {
            SetStatus("Renueva antes la sesion de GlobalLeaks para importar denuncias.", "alert-warning");
            return;
        }

        ImportBusy = true;
        var importedCount = 0;
        var errors = new List<string>();
        var importWarnings = new List<string>();

        try
        {
            var selectedReports = Reports
                .Where(report => SelectedIds.Contains(report.Id))
                .OrderBy(report => report.Progressive ?? 0)
                .ToList();

            using var busy = Busy.Show(
                "Importando denuncias",
                $"Descargando y procesando {selectedReports.Count} denuncia(s) desde GlobalLeaks.",
                selectedReports.Count);

            var processed = 0;
            foreach (var report in selectedReports)
            {
                processed++;
                Busy.Update(
                    message: $"Procesando denuncia #{report.Progressive ?? 0}.",
                    detail: "Descarga, analisis del report y guardado seguro en la base de datos.",
                    current: processed,
                    total: selectedReports.Count);

                try
                {
                    var result = await ApiDenuncias.ImportReportAsync(report, CancellationToken.None);
                    importedCount += result.ImportedCount;
                    errors.AddRange(result.Errors.Select(error => $"#{report.Progressive ?? 0}: {error}"));
                    if (result.Warnings is not null)
                    {
                        importWarnings.AddRange(result.Warnings.Select(warning => $"#{report.Progressive ?? 0}: {warning}"));
                    }
                }
                catch (UnauthorizedAccessException ex)
                {
                    await LoadSessionStateAsync();
                    SetStatus(ex.Message, "alert-warning");
                    break;
                }
                catch (Exception ex)
                {
                    errors.Add($"#{report.Progressive ?? 0}: {ex.Message}");
                }
            }

            SelectedIds.Clear();
            await LoadReportsAsync();

            var warnings = selectedReports
                .Where(report => report.DownloadedByAnotherUser || report.AlreadyInGestiona)
                .Select(report => $"#{report.Progressive ?? 0}: {report.TrackingNote}")
                .Where(message => !string.IsNullOrWhiteSpace(message))
                .ToList();
            warnings.AddRange(importWarnings);

            if (errors.Count == 0 && warnings.Count == 0)
            {
                SetStatus($"Se han importado {importedCount} denuncia(s) desde GlobalLeaks.", "alert-success");
            }
            else
            {
                var parts = new List<string>
                {
                    $"Se han importado {importedCount} denuncia(s)."
                };

                if (warnings.Count > 0)
                {
                    parts.Add($"Avisos: {string.Join(" | ", warnings)}");
                }

                if (errors.Count > 0)
                {
                    parts.Add($"Incidencias: {string.Join(" | ", errors)}");
                }

                SetStatus(string.Join(" ", parts), errors.Count == 0 ? "alert-warning" : "alert-danger");
            }
        }
        finally
        {
            ImportBusy = false;
        }
    }

    private async Task OpenReportDetailAsync(ReportDto report)
    {
        if (!CanUseGlobalLeaks)
        {
            SetStatus("Renueva antes la sesion de GlobalLeaks para consultar el detalle.", "alert-warning");
            return;
        }

        DetailModalVisible = true;
        DetailReport = report;
        DetailData = null;
        DetailError = string.Empty;
        DetailBusy = true;
        await SetBodyScrollLockAsync(true);

        try
        {
            using var busy = Busy.Show(
                "Leyendo detalle de denuncia",
                "Consultando mensajes y ficheros para identificar que contenido es nuevo.");

            if (!DetailCache.TryGetValue(report.Id, out var cachedDetail))
            {
                cachedDetail = await ApiDenuncias.GetReportDetailAsync(report.Id, report.LastAccess, CancellationToken.None);
                DetailCache[report.Id] = cachedDetail;
            }

            DetailData = cachedDetail;
        }
        catch (UnauthorizedAccessException ex)
        {
            await LoadSessionStateAsync();
            DetailError = ex.Message;
        }
        catch (Exception ex)
        {
            DetailError = ex.Message;
        }
        finally
        {
            DetailBusy = false;
        }
    }

    private async Task CloseReportDetail()
    {
        DetailModalVisible = false;
        DetailReport = null;
        DetailData = null;
        DetailError = string.Empty;
        await SetBodyScrollLockAsync(false);
    }

    public async ValueTask DisposeAsync()
    {
        await SetBodyScrollLockAsync(false);
    }

    private async Task SetBodyScrollLockAsync(bool locked)
    {
        try
        {
            await JSRuntime.InvokeVoidAsync("appSetBodyScrollLock", locked);
        }
        catch
        {
            // Si el circuito se esta cerrando, solo aseguramos que no reviente el componente.
        }
    }

    private void ApplyFilters()
    {
        IEnumerable<ReportDto> filtered = Reports;
        FilterWarningMessage = string.Empty;
        FilterWarningCss = "alert-warning";

        filtered = Filter switch
        {
            "new" => filtered.Where(report => string.IsNullOrWhiteSpace(report.AccessDate) || string.Equals(report.Status, "new", StringComparison.OrdinalIgnoreCase)),
            "updated" => filtered.Where(report => report.Updated),
            _ => filtered
        };

        filtered = ApplyDateScope(filtered);

        if (!string.IsNullOrWhiteSpace(SelectedChannel))
        {
            filtered = filtered.Where(report => string.Equals(report.ContextId, SelectedChannel, StringComparison.OrdinalIgnoreCase));
        }

        if (!string.IsNullOrWhiteSpace(SearchTerm))
        {
            var search = SearchTerm.Trim();
            filtered = filtered.Where(report =>
                (report.Progressive?.ToString() ?? string.Empty).Contains(search, StringComparison.OrdinalIgnoreCase) ||
                (report.ContextName ?? report.ContextId ?? string.Empty).Contains(search, StringComparison.OrdinalIgnoreCase));
        }

        VisibleReports = filtered
            .OrderByDescending(report => report.Progressive ?? 0)
            .ToList();

        var validIds = Reports.Select(report => report.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
        SelectedIds.RemoveWhere(id => !validIds.Contains(id));
    }

    private IEnumerable<ReportDto> ApplyDateScope(IEnumerable<ReportDto> reports)
    {
        if (DateScope == "since_mine")
        {
            if (UserInboxState.LastDownloadedReportMomentUtc is null)
            {
                FilterWarningMessage = "Este usuario aun no tiene descargas previas. Se muestran todas las denuncias del filtro seleccionado.";
                FilterWarningCss = "alert-info";
                return reports;
            }

            var lastMoment = UserInboxState.LastDownloadedReportMomentUtc.Value;
            return reports.Where(report => GetEffectiveMoment(report) is DateTimeOffset moment && moment > lastMoment);
        }

        if (DateScope == "range")
        {
            var hasFrom = DateOnly.TryParseExact(DateFrom, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var fromDate);
            var hasTo = DateOnly.TryParseExact(DateTo, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var toDate);

            if (hasFrom && hasTo && fromDate > toDate)
            {
                FilterWarningMessage = "La fecha inicial no puede ser posterior a la final.";
                FilterWarningCss = "alert-danger";
                return Enumerable.Empty<ReportDto>();
            }

            var filtered = reports.Where(report =>
            {
                var moment = GetEffectiveMoment(report);
                if (moment is null)
                {
                    return false;
                }

                var reportDate = DateOnly.FromDateTime(moment.Value.UtcDateTime);
                var matchesFrom = !hasFrom || reportDate >= fromDate;
                var matchesTo = !hasTo || reportDate <= toDate;
                return matchesFrom && matchesTo;
            });

            if (hasFrom &&
                UserInboxState.LastDownloadedReportMomentUtc is DateTimeOffset lastDownloaded &&
                fromDate > DateOnly.FromDateTime(lastDownloaded.UtcDateTime))
            {
                var gapExists = Reports.Any(report =>
                {
                    var moment = GetEffectiveMoment(report);
                    if (moment is null)
                    {
                        return false;
                    }

                    var reportDate = DateOnly.FromDateTime(moment.Value.UtcDateTime);
                    return moment.Value > lastDownloaded && reportDate < fromDate;
                });

                if (gapExists)
                {
                    FilterWarningMessage =
                        $"Ojo: entre tu ultima descarga ({lastDownloaded.ToLocalTime():dd/MM/yyyy HH:mm}) y el inicio del intervalo ({fromDate:dd/MM/yyyy}) hay denuncias o actualizaciones sin revisar.";
                }
            }

            return filtered;
        }

        return reports;
    }

    private void ToggleSelection(string reportId, ChangeEventArgs args)
    {
        var isChecked = GetCheckedValue(args);
        if (isChecked)
        {
            SelectedIds.Add(reportId);
        }
        else
        {
            SelectedIds.Remove(reportId);
        }

        StateHasChanged();
    }

    private void ToggleSelectAll()
    {
        var shouldSelect = !VisibleReports.All(report => SelectedIds.Contains(report.Id));

        foreach (var report in VisibleReports)
        {
            if (shouldSelect)
            {
                SelectedIds.Add(report.Id);
            }
            else
            {
                SelectedIds.Remove(report.Id);
            }
        }

        StateHasChanged();
    }

    private void OnFilterChanged(ChangeEventArgs args)
    {
        Filter = args.Value?.ToString() ?? "all";
        ApplyFilters();
    }

    private void OnDateScopeChanged(ChangeEventArgs args)
    {
        DateScope = args.Value?.ToString() ?? "all";
        if (DateScope != "range")
        {
            DateFrom = string.Empty;
            DateTo = string.Empty;
        }

        ApplyFilters();
    }

    private void OnDateFromChanged(ChangeEventArgs args)
    {
        DateFrom = args.Value?.ToString() ?? string.Empty;
        ApplyFilters();
    }

    private void OnDateToChanged(ChangeEventArgs args)
    {
        DateTo = args.Value?.ToString() ?? string.Empty;
        ApplyFilters();
    }

    private void OnChannelChanged(ChangeEventArgs args)
    {
        SelectedChannel = args.Value?.ToString() ?? string.Empty;
        ApplyFilters();
    }

    private void OnSearchChanged(ChangeEventArgs args)
    {
        SearchTerm = args.Value?.ToString() ?? string.Empty;
        ApplyFilters();
    }

    private void SetStatus(string message, string cssClass)
    {
        StatusMessage = message;
        StatusCss = cssClass;
    }

    private static bool GetCheckedValue(ChangeEventArgs args)
    {
        return args.Value switch
        {
            bool b => b,
            string s when bool.TryParse(s, out var parsed) => parsed,
            _ => false
        };
    }

    private static string FormatDate(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return "-";
        }

        return DateTimeOffset.TryParse(value, out var parsed)
            ? parsed.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
            : "-";
    }

    private static string FormatBytes(long? value)
    {
        if (value is null or <= 0)
        {
            return "Tamano no disponible";
        }

        var bytes = value.Value;
        if (bytes < 1024)
        {
            return $"{bytes} B";
        }

        if (bytes < 1024 * 1024)
        {
            return $"{bytes / 1024d:0.#} KB";
        }

        return $"{bytes / 1024d / 1024d:0.#} MB";
    }

    private static string GetCommentAuthorLabel(string? type)
        => string.Equals(type, "whistleblower", StringComparison.OrdinalIgnoreCase)
            ? "Denunciante"
            : string.Equals(type, "receiver", StringComparison.OrdinalIgnoreCase)
                ? "Receptor"
                : "Comentario";

    private static DateTimeOffset? GetEffectiveMoment(ReportDto report)
    {
        return ParseDate(report.UpdateDate) ?? ParseDate(report.CreationDate);
    }

    private static DateTimeOffset? ParseDate(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        return DateTimeOffset.TryParse(value.Replace("Z", "+00:00", StringComparison.Ordinal), out var parsed)
            ? parsed
            : null;
    }

    private static string GetStatusLabel(ReportDto report)
    {
        if (string.IsNullOrWhiteSpace(report.AccessDate))
        {
            return "Sin leer";
        }

        if (report.Updated)
        {
            return "Actualizada";
        }

        return string.Equals(report.Status, "closed", StringComparison.OrdinalIgnoreCase)
            ? "Cerrada"
            : "Abierta";
    }

    private static string GetStatusBadgeCss(ReportDto report)
    {
        if (string.IsNullOrWhiteSpace(report.AccessDate))
        {
            return "bg-warning text-dark";
        }

        if (report.Updated)
        {
            return "bg-info text-dark";
        }

        return string.Equals(report.Status, "closed", StringComparison.OrdinalIgnoreCase)
            ? "bg-secondary"
            : "bg-primary";
    }

    private static string GetTrackingLabel(ReportDto report)
    {
        if (report.AlreadyInGestiona)
        {
            return "Expediente creado";
        }

        if (report.DownloadedByAnotherUser)
        {
            return "Descargada por otro";
        }

        if (report.DownloadedByCurrentUser)
        {
            return "Descargada por ti";
        }

        if (report.AlreadyImported)
        {
            return "Ya incorporada";
        }

        return "Pendiente";
    }

    private static string GetTrackingBadgeCss(ReportDto report)
    {
        if (report.AlreadyInGestiona)
        {
            return "bg-success";
        }

        if (report.DownloadedByAnotherUser)
        {
            return "bg-warning text-dark";
        }

        if (report.DownloadedByCurrentUser)
        {
            return "bg-secondary";
        }

        if (report.AlreadyImported)
        {
            return "bg-info text-dark";
        }

        return "bg-light text-dark";
    }

    private static string? GetReportRowCss(ReportDto report)
    {
        if (report.AlreadyInGestiona)
        {
            return "table-success";
        }

        if (report.DownloadedByAnotherUser)
        {
            return "table-warning";
        }

        return null;
    }
}
