using System.Diagnostics;
using System.Globalization;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using ApiDenuncias.Configuration;
using GestionaDenuncias.Shared.Models;
using Konscious.Security.Cryptography;
using Microsoft.Extensions.Options;

namespace ApiDenuncias.Services;

public sealed record PreparedGlobalLeaksCredentials(string Username, string FinalPassword, string TokenAnswer);

public sealed class GlobalLeaksClient
{
    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
    private readonly HttpClient _httpClient;
    private readonly ILogger<GlobalLeaksClient> _logger;
    private readonly GlobalLeaksOptions _options;

    public GlobalLeaksClient(IOptions<GlobalLeaksOptions> options, ILogger<GlobalLeaksClient> logger)
    {
        _options = options.Value;
        _logger = logger;

        var handler = new HttpClientHandler();
        if (_options.AllowInvalidCertificate)
        {
            _logger.LogWarning("GlobalLeaks permite certificados TLS no validos. Usar solo temporalmente en PRE.");
            handler.ServerCertificateCustomValidationCallback =
                HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
        }

        _httpClient = new HttpClient(handler)
        {
            BaseAddress = new Uri(_options.BaseUrl.TrimEnd('/')),
            Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds),
        };

        if (!string.IsNullOrWhiteSpace(_options.HostHeader))
        {
            _httpClient.DefaultRequestHeaders.Host = _options.HostHeader.Trim();
        }
    }

    public async Task<GlSession> LoginAsync(
        string username,
        string password,
        string authcode,
        CancellationToken cancellationToken)
    {
        var prepared = await PrepareLoginAsync(username, password, cancellationToken);
        return await CompleteLoginAsync(
            prepared.Username,
            prepared.FinalPassword,
            prepared.TokenAnswer,
            authcode,
            cancellationToken);
    }

    public async Task<PreparedGlobalLeaksCredentials> PrepareLoginAsync(
        string username,
        string password,
        CancellationToken cancellationToken)
    {
        var loginWatch = Stopwatch.StartNew();
        _logger.LogInformation(
            "GlobalLeaks login: iniciando para {Username}. BaseUrl={BaseUrl}",
            username,
            _options.BaseUrl);


        using var typeRequest = CreateRequest(HttpMethod.Post, "/api/auth/type");
        typeRequest.Content = CreateJsonContent(new { username });
        using var typeResponse = await SendLoginRequestAsync(typeRequest, "/api/auth/type", cancellationToken);
        await EnsureSuccessOrThrowAsync(typeResponse, "/api/auth/type", cancellationToken);

        var authType = await typeResponse.Content.ReadFromJsonAsync<AuthTypeResponse>(JsonOptions, cancellationToken)
            ?? throw new GlobalLeaksValidationException("No se pudo obtener el tipo de autenticación.", 502);

        var passwordWatch = Stopwatch.StartNew();
        _logger.LogInformation(
            "GlobalLeaks login: preparando credenciales para {Username}. AuthType={AuthType}",
            username,
            authType.Type);
        var finalPassword = authType.Type == "key"
            ? DerivePassword(password, authType.Salt)
            : password;
        _logger.LogInformation(
            "GlobalLeaks login: credenciales preparadas para {Username} en {ElapsedMs} ms",
            username,
            passwordWatch.ElapsedMilliseconds);

        var tokenAnswer = await PrepareProofOfWorkAsync(username, cancellationToken);

        _logger.LogInformation(
            "GlobalLeaks login: credenciales y proof-of-work preparados para {Username}. Tiempo total={ElapsedMs} ms",
            username,
            loginWatch.ElapsedMilliseconds);

        return new PreparedGlobalLeaksCredentials(username, finalPassword, tokenAnswer);
    }

    public async Task<GlSession> CompleteLoginAsync(
        string username,
        string finalPassword,
        string authcode,
        CancellationToken cancellationToken)
    {
        var tokenAnswer = await PrepareProofOfWorkAsync(username, cancellationToken);
        return await CompleteLoginAsync(username, finalPassword, tokenAnswer, authcode, cancellationToken);
    }

    public async Task<GlSession> CompleteLoginAsync(
        string username,
        string finalPassword,
        string tokenAnswer,
        string authcode,
        CancellationToken cancellationToken)
    {
        var loginWatch = Stopwatch.StartNew();
        _logger.LogInformation(
            "GlobalLeaks login: enviando autenticacion final para {Username}. AuthcodeLength={AuthcodeLength}",
            username,
            authcode?.Length ?? 0);

        var currentTokenAnswer = tokenAnswer;
        for (var attempt = 0; attempt < 2; attempt++)
        {
            using var authRequest = CreateRequest(
                HttpMethod.Post,
                $"/api/auth/authentication?token={Uri.EscapeDataString(currentTokenAnswer)}");
            authRequest.Content = CreateJsonContent(new
            {
                tid = 1,
                username,
                password = finalPassword,
                authcode = authcode?.Trim() ?? string.Empty,
            });
            authRequest.Headers.TryAddWithoutValidation("X-Token", currentTokenAnswer);

            using var authResponse = await SendLoginRequestAsync(authRequest, "/api/auth/authentication", cancellationToken);
            if (authResponse.IsSuccessStatusCode)
            {
                var authBody = await authResponse.Content.ReadAsStringAsync(cancellationToken);
                var session = ParseAuthSession(authBody, username);
                _logger.LogInformation(
                    "Login GlobalLeaks correcto para {Username}. Rol: {Role}. Tiempo total={ElapsedMs} ms",
                    session.Username,
                    session.Role ?? "(sin rol)",
                    loginWatch.ElapsedMilliseconds);
                return session;
            }

            var body = await ReadBodySafeAsync(authResponse, cancellationToken);
            if (attempt == 0 && IsMissingTokenOrSessionError(body))
            {
                _logger.LogWarning(
                    "GlobalLeaks rechazo el token preparado para {Username}; se generara un token nuevo y se reintentara la autenticacion.",
                    username);
                currentTokenAnswer = await PrepareProofOfWorkAsync(username, cancellationToken);
                continue;
            }

            throw authResponse.StatusCode switch
            {
                HttpStatusCode.Unauthorized => new GlobalLeaksValidationException(
                    "Credenciales incorrectas o codigo 2FA invalido.",
                    StatusCodes.Status401Unauthorized),
                (HttpStatusCode)429 => new GlobalLeaksValidationException(
                    "Demasiados intentos en GlobalLeaks. Espera unos minutos.",
                    StatusCodes.Status429TooManyRequests),
                HttpStatusCode.NotAcceptable when IsTwoFactorRequiredError(body) =>
                    new GlobalLeaksValidationException(
                        "Codigo 2FA invalido, caducado o ya utilizado. Introduce el codigo actual de la app autenticadora.",
                        StatusCodes.Status406NotAcceptable),
                _ => new GlobalLeaksValidationException(
                    string.IsNullOrWhiteSpace(body)
                        ? $"Login fallido (codigo {(int)authResponse.StatusCode})."
                        : $"Login fallido (codigo {(int)authResponse.StatusCode}): {body}",
                    (int)authResponse.StatusCode),
            };
        }

        throw new GlobalLeaksValidationException("Login fallido: no se pudo completar la autenticacion.", 502);
    }

    private async Task<string> PrepareProofOfWorkAsync(string username, CancellationToken cancellationToken)
    {
        using var tokenRequest = CreateRequest(HttpMethod.Post, "/api/auth/token");
        using var tokenResponse = await SendLoginRequestAsync(tokenRequest, "/api/auth/token", cancellationToken);
        await EnsureSuccessOrThrowAsync(tokenResponse, "/api/auth/token", cancellationToken);
        var tokenData = await tokenResponse.Content.ReadFromJsonAsync<TokenResponse>(JsonOptions, cancellationToken)
            ?? throw new GlobalLeaksValidationException("No se pudo obtener el reto criptografico.", 502);

        var proofWatch = Stopwatch.StartNew();
        _logger.LogInformation("GlobalLeaks login: resolviendo proof-of-work para {Username}", username);
        var tokenAnswer = SolveProofOfWork(tokenData.Id, tokenData.Salt, cancellationToken);
        _logger.LogInformation(
            "GlobalLeaks login: proof-of-work resuelto para {Username} en {ElapsedMs} ms",
            username,
            proofWatch.ElapsedMilliseconds);
        return tokenAnswer;
    }

    public async Task<IReadOnlyList<ContextDto>> GetContextsAsync(
        string sessionId,
        CancellationToken cancellationToken)
    {
        using var request = CreateRequest(HttpMethod.Get, "/api/public");
        using var response = await SendGlRequestAsync(request, cancellationToken);
        var body = await response.Content.ReadAsStringAsync(cancellationToken);
        var contexts = ParseContexts(body);
        _logger.LogInformation("GlobalLeaks /api/public devolvió {Count} contextos", contexts.Count);
        return contexts;
    }

    public async Task<IReadOnlyList<ReportDto>> GetReportsAsync(
        string sessionId,
        string? filter,
        string? dateFrom,
        string? dateTo,
        CancellationToken cancellationToken,
        IReadOnlyList<ContextDto>? contexts = null)
    {
        filter ??= "all";

        using var reportsRequest = CreateAuthenticatedRequest(HttpMethod.Get, "/api/recipient/rtips", sessionId);
        using var reportsResponse = await SendGlRequestAsync(reportsRequest, cancellationToken);
        var body = await reportsResponse.Content.ReadAsStringAsync(cancellationToken);
        var tips = ParseReports(body);
        _logger.LogInformation("GlobalLeaks /api/recipient/rtips devolvió {Count} denuncias", tips.Count);

        var availableContexts = contexts ?? await GetContextsAsync(sessionId, cancellationToken);
        var contextNames = availableContexts.ToDictionary(c => c.Id, c => c.Name, StringComparer.OrdinalIgnoreCase);

        IEnumerable<RawReport> filtered = tips;

        filtered = filter switch
        {
            "new" => filtered.Where(t => string.IsNullOrWhiteSpace(t.AccessDate) || t.Status == "new"),
            "updated" or "updated_citizen" or "updated_receiver" => filtered.Where(t => t.Updated),
            _ => filtered,
        };

        if (!string.IsNullOrWhiteSpace(dateFrom))
        {
            if (!DateOnly.TryParseExact(dateFrom, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var from))
            {
                throw new GlobalLeaksValidationException("Formato de fecha inválido para date_from (use YYYY-MM-DD)");
            }

            filtered = filtered.Where(t =>
            {
                var date = ParseDate(t.CreationDate);
                return date is not null && DateOnly.FromDateTime(date.Value.DateTime) >= from;
            });
        }

        if (!string.IsNullOrWhiteSpace(dateTo))
        {
            if (!DateOnly.TryParseExact(dateTo, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var to))
            {
                throw new GlobalLeaksValidationException("Formato de fecha inválido para date_to (use YYYY-MM-DD)");
            }

            filtered = filtered.Where(t =>
            {
                var date = ParseDate(t.CreationDate);
                return date is not null && DateOnly.FromDateTime(date.Value.DateTime) <= to;
            });
        }

        return filtered
            .OrderByDescending(t => t.Progressive ?? 0)
            .Select(t => new ReportDto
            {
                Id = t.Id,
                Progressive = t.Progressive,
                ContextId = t.ContextId,
                ContextName = t.ContextId is not null && contextNames.TryGetValue(t.ContextId, out var name)
                    ? name
                    : t.ContextId,
                CreationDate = t.CreationDate,
                UpdateDate = t.UpdateDate,
                ExpirationDate = t.ExpirationDate,
                ReminderDate = t.ReminderDate,
                AccessDate = t.AccessDate,
                LastAccess = t.LastAccess,
                Status = t.Status,
                Updated = t.Updated,
                Label = t.Label,
            })
            .ToArray();
    }

    public async Task<ReportDetailDto> GetReportDetailAsync(
        string sessionId,
        string reportId,
        string? lastAccess,
        CancellationToken cancellationToken)
    {
        ValidateUuid(reportId);

        using var detailRequest = CreateAuthenticatedRequest(HttpMethod.Get, $"/api/recipient/rtips/{reportId}", sessionId);
        using var detailResponse = await SendGlRequestAsync(detailRequest, cancellationToken);

        var contentType = detailResponse.Content.Headers.ContentType?.MediaType ?? string.Empty;
        var content = await detailResponse.Content.ReadAsByteArrayAsync(cancellationToken);
        if (!contentType.Contains("json", StringComparison.OrdinalIgnoreCase) || content.Length == 0)
        {
            throw new GlobalLeaksValidationException(
                "No se pudo leer el detalle de la denuncia. Puede estar cifrada sin clave disponible en el servidor.",
                422);
        }

        using var document = JsonDocument.Parse(content);
        return ParseReportDetail(reportId, lastAccess, document.RootElement);
    }

    public async Task<FileDownloadResult> DownloadReportZipAsync(
        string sessionId,
        string reportId,
        CancellationToken cancellationToken)
    {
        ValidateUuid(reportId);

        using var request = CreateAuthenticatedRequest(
            HttpMethod.Get,
            $"/api/recipient/rtips/{reportId}/export",
            sessionId);

        using var response = await SendGlRequestAsync(
            request,
            cancellationToken,
            HttpCompletionOption.ResponseHeadersRead);

        var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
        if (content.Length > _options.MaxDownloadBytes)
        {
            throw new GlobalLeaksValidationException("El fichero supera el límite de 500 MB.", 413);
        }

        var fileName = SanitizeFileName(
            ExtractFileName(response.Content.Headers.ContentDisposition),
            $"report-{reportId}.zip");

        return new FileDownloadResult(content, fileName);
    }

    public async Task<FileDownloadResult> ExportReportJsonAsync(
        string sessionId,
        string reportId,
        CancellationToken cancellationToken)
    {
        ValidateUuid(reportId);

        using var request = CreateAuthenticatedRequest(HttpMethod.Get, $"/api/recipient/rtips/{reportId}", sessionId);
        using var response = await SendGlRequestAsync(request, cancellationToken);

        var contentType = response.Content.Headers.ContentType?.MediaType ?? string.Empty;
        var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
        if (!contentType.Contains("json", StringComparison.OrdinalIgnoreCase) || content.Length == 0)
        {
            throw new GlobalLeaksValidationException(
                "GlobalLeaks no devolvió JSON (tip sin clave de descifrado).",
                422);
        }

        using var document = JsonDocument.Parse(content);
        var progressive = document.RootElement.TryGetProperty("progressive", out var value) && value.TryGetInt32(out var number)
            ? number.ToString(CultureInfo.InvariantCulture)
            : reportId[..8];

        return new FileDownloadResult(content, $"report-{progressive}.json");
    }

    private async Task<HttpResponseMessage> SendLoginRequestAsync(
        HttpRequestMessage request,
        string endpoint,
        CancellationToken cancellationToken)
    {
        var stepWatch = Stopwatch.StartNew();
        _logger.LogInformation("GlobalLeaks login: llamando a {Endpoint}", endpoint);

        try
        {
            var response = await _httpClient.SendAsync(request, cancellationToken);
            _logger.LogInformation(
                "GlobalLeaks login: {Endpoint} respondio {StatusCode} en {ElapsedMs} ms",
                endpoint,
                (int)response.StatusCode,
                stepWatch.ElapsedMilliseconds);
            return response;
        }
        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
        {
            _logger.LogWarning(
                "GlobalLeaks login: timeout en {Endpoint} tras {ElapsedMs} ms",
                endpoint,
                stepWatch.ElapsedMilliseconds);
            throw;
        }
        catch (Exception ex)
        {
            _logger.LogError(
                ex,
                "GlobalLeaks login: error en {Endpoint} tras {ElapsedMs} ms",
                endpoint,
                stepWatch.ElapsedMilliseconds);
            throw;
        }
    }

    private async Task<HttpResponseMessage> SendGlRequestAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken,
        HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
    {
        var response = await _httpClient.SendAsync(request, completionOption, cancellationToken);
        if ((int)response.StatusCode == 412)
        {
            response.Dispose();
            throw new GlobalLeaksSessionExpiredException();
        }

        if (!response.IsSuccessStatusCode)
        {
            var message = $"Error de GlobalLeaks (código {(int)response.StatusCode}).";
            response.Dispose();
            throw new GlobalLeaksValidationException(message, (int)response.StatusCode);
        }

        return response;
    }

    private static HttpRequestMessage CreateRequest(HttpMethod method, string path)
    {
        var request = new HttpRequestMessage(method, path)
        {
            Version = HttpVersion.Version11,
            VersionPolicy = HttpVersionPolicy.RequestVersionOrLower,
        };

        request.Headers.TryAddWithoutValidation("Accept", "*/*");
        request.Headers.TryAddWithoutValidation("User-Agent", "python-requests/2.32.5");
        return request;
    }

    private static HttpRequestMessage CreateAuthenticatedRequest(HttpMethod method, string path, string sessionId)
    {
        var request = CreateRequest(method, path);
        request.Headers.Add("X-Session", sessionId);
        return request;
    }

    private static ByteArrayContent CreateJsonContent<T>(T payload)
    {
        var bytes = JsonSerializer.SerializeToUtf8Bytes(payload, JsonOptions);
        var content = new ByteArrayContent(bytes);
        content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
        return content;
    }

    private static string SolveProofOfWork(string tokenId, string tokenSalt, CancellationToken cancellationToken)
    {
        var idBytes = Encoding.UTF8.GetBytes(tokenId);
        var saltBytes = Convert.FromBase64String(tokenSalt).Take(16).ToArray();
        var n = 0;

        while (true)
        {
            if ((n & 15) == 0)
            {
                cancellationToken.ThrowIfCancellationRequested();
            }

            var input = idBytes.Concat(Encoding.UTF8.GetBytes(n.ToString(CultureInfo.InvariantCulture))).ToArray();
            var hash = ComputeArgon2(input, saltBytes, iterations: 1, memoryKb: 1024);
            if (hash[^1] == 0)
            {
                return $"{tokenId}:{n}";
            }

            n++;
        }
    }

    private static string DerivePassword(string password, string salt)
    {
        var saltBytes = Convert.FromBase64String(salt).Take(16).ToArray();
        var hash = ComputeArgon2(Encoding.UTF8.GetBytes(password), saltBytes, iterations: 16, memoryKb: 131072);
        return Convert.ToBase64String(hash);
    }

    private static byte[] ComputeArgon2(byte[] input, byte[] salt, int iterations, int memoryKb)
    {
        var argon2 = new Argon2id(input)
        {
            Salt = salt,
            DegreeOfParallelism = 1,
            Iterations = iterations,
            MemorySize = memoryKb,
        };

        return argon2.GetBytes(32);
    }

    private static void ValidateUuid(string value)
    {
        if (!Guid.TryParse(value, out _))
        {
            throw new GlobalLeaksValidationException("ID de denuncia inválido");
        }
    }

    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 ExtractName(JsonElement? name, string fallback)
    {
        if (name is null)
        {
            return fallback;
        }

        if (name.Value.ValueKind == JsonValueKind.String)
        {
            return name.Value.GetString() ?? fallback;
        }

        if (name.Value.ValueKind == JsonValueKind.Object)
        {
            if (name.Value.TryGetProperty("es", out var es) && es.GetString() is { Length: > 0 } esName)
            {
                return esName;
            }

            if (name.Value.TryGetProperty("en", out var en) && en.GetString() is { Length: > 0 } enName)
            {
                return enName;
            }

            foreach (var property in name.Value.EnumerateObject())
            {
                if (!string.IsNullOrWhiteSpace(property.Value.GetString()))
                {
                    return property.Value.GetString()!;
                }
            }
        }

        return fallback;
    }

    private static List<ContextDto> ParseContexts(string body)
    {
        using var document = JsonDocument.Parse(body);
        var contextsElement = document.RootElement;

        if (contextsElement.ValueKind == JsonValueKind.Object &&
            contextsElement.TryGetProperty("contexts", out var contextsProperty))
        {
            contextsElement = contextsProperty;
        }

        if (contextsElement.ValueKind != JsonValueKind.Array)
        {
            return [];
        }

        var contexts = new List<ContextDto>();
        foreach (var item in contextsElement.EnumerateArray())
        {
            var id = GetString(item, "id") ?? string.Empty;
            if (string.IsNullOrWhiteSpace(id))
            {
                continue;
            }

            JsonElement? name = null;
            if (item.TryGetProperty("name", out var nameProp))
            {
                name = nameProp;
            }

            contexts.Add(new ContextDto(id, ExtractName(name, id)));
        }

        return contexts;
    }

    private static List<RawReport> ParseReports(string body)
    {
        using var document = JsonDocument.Parse(body);
        var root = document.RootElement;
        var array = root.ValueKind == JsonValueKind.Array
            ? root
            : FindArrayProperty(root, "rtips", "tips", "items", "data", "entries", "results");

        if (array is null || array.Value.ValueKind != JsonValueKind.Array)
        {
            return [];
        }

        var reports = new List<RawReport>();
        foreach (var item in array.Value.EnumerateArray())
        {
            var id = GetString(item, "id") ?? string.Empty;
            if (string.IsNullOrWhiteSpace(id))
            {
                continue;
            }

            reports.Add(new RawReport
            {
                Id = id,
                Progressive = GetInt32(item, "progressive"),
                ContextId = GetString(item, "context_id", "contextId"),
                CreationDate = GetString(item, "creation_date", "creationDate"),
                UpdateDate = GetString(item, "update_date", "updateDate"),
                ExpirationDate = GetString(item, "expiration_date", "expirationDate"),
                ReminderDate = GetString(item, "reminder_date", "reminderDate"),
                AccessDate = GetString(item, "access_date", "accessDate"),
                LastAccess = GetString(item, "last_access", "lastAccess"),
                Status = GetString(item, "status"),
                Updated = GetBool(item, "updated"),
                Label = GetString(item, "label"),
            });
        }

        return reports;
    }

    private static ReportDetailDto ParseReportDetail(string reportId, string? lastAccess, JsonElement root)
    {
        var lastAccessDate = ParseDate(lastAccess);

        bool IsNew(string? value)
        {
            if (lastAccessDate is null)
            {
                return true;
            }

            var itemDate = ParseDate(value);
            return itemDate is not null && itemDate > lastAccessDate;
        }

        var comments = EnumerateArray(root, "comments")
            .Select(item => new ReportCommentDto(
                GetString(item, "id"),
                GetString(item, "type"),
                GetString(item, "content", "text", "message"),
                GetString(item, "creation_date", "creationDate"),
                IsNew(GetString(item, "creation_date", "creationDate"))))
            .ToArray();

        var whistleblowerFiles = EnumerateArray(root, "wbfiles", "files")
            .Select(item => new ReportFileDto(
                GetString(item, "id"),
                GetLocalizedString(item, "name", "file_name", "filename"),
                GetInt64(item, "size"),
                GetString(item, "content_type", "contentType", "mime_type", "mimetype"),
                GetString(item, "creation_date", "creationDate"),
                IsNew(GetString(item, "creation_date", "creationDate"))))
            .ToArray();

        var receiverFiles = EnumerateArray(root, "rfiles")
            .Select(item => new ReportFileDto(
                GetString(item, "id"),
                GetLocalizedString(item, "name", "file_name", "filename"),
                GetInt64(item, "size"),
                GetString(item, "content_type", "contentType", "mime_type", "mimetype"),
                GetString(item, "creation_date", "creationDate"),
                IsNew(GetString(item, "creation_date", "creationDate"))))
            .ToArray();

        return new ReportDetailDto(reportId, lastAccess, comments, whistleblowerFiles, receiverFiles);
    }

    private static IEnumerable<JsonElement> EnumerateArray(JsonElement root, params string[] names)
    {
        if (root.ValueKind != JsonValueKind.Object)
        {
            yield break;
        }

        foreach (var name in names)
        {
            if (root.TryGetProperty(name, out var property) && property.ValueKind == JsonValueKind.Array)
            {
                foreach (var item in property.EnumerateArray())
                {
                    yield return item;
                }

                yield break;
            }
        }
    }

    private static JsonElement? FindArrayProperty(JsonElement element, params string[] names)
    {
        if (element.ValueKind != JsonValueKind.Object)
        {
            return null;
        }

        foreach (var name in names)
        {
            if (element.TryGetProperty(name, out var property) && property.ValueKind == JsonValueKind.Array)
            {
                return property;
            }
        }

        return null;
    }

    private static string? GetString(JsonElement element, params string[] names)
    {
        foreach (var name in names)
        {
            if (element.TryGetProperty(name, out var property))
            {
                return property.ValueKind switch
                {
                    JsonValueKind.String => property.GetString(),
                    JsonValueKind.Number => property.GetRawText(),
                    _ => null,
                };
            }
        }

        return null;
    }

    private static string? GetLocalizedString(JsonElement element, params string[] names)
    {
        foreach (var name in names)
        {
            if (!element.TryGetProperty(name, out var property))
            {
                continue;
            }

            if (property.ValueKind == JsonValueKind.String)
            {
                return property.GetString();
            }

            if (property.ValueKind == JsonValueKind.Object)
            {
                var value = ExtractName(property, string.Empty);
                return string.IsNullOrWhiteSpace(value) ? null : value;
            }
        }

        return null;
    }

    private static int? GetInt32(JsonElement element, params string[] names)
    {
        foreach (var name in names)
        {
            if (element.TryGetProperty(name, out var property))
            {
                if (property.ValueKind == JsonValueKind.Number && property.TryGetInt32(out var value))
                {
                    return value;
                }

                if (property.ValueKind == JsonValueKind.String &&
                    int.TryParse(property.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
                {
                    return value;
                }
            }
        }

        return null;
    }

    private static long? GetInt64(JsonElement element, params string[] names)
    {
        foreach (var name in names)
        {
            if (element.TryGetProperty(name, out var property))
            {
                if (property.ValueKind == JsonValueKind.Number && property.TryGetInt64(out var value))
                {
                    return value;
                }

                if (property.ValueKind == JsonValueKind.String &&
                    long.TryParse(property.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
                {
                    return value;
                }
            }
        }

        return null;
    }

    private static bool GetBool(JsonElement element, params string[] names)
    {
        foreach (var name in names)
        {
            if (element.TryGetProperty(name, out var property))
            {
                if (property.ValueKind is JsonValueKind.True or JsonValueKind.False)
                {
                    return property.GetBoolean();
                }

                if (property.ValueKind == JsonValueKind.String &&
                    bool.TryParse(property.GetString(), out var value))
                {
                    return value;
                }
            }
        }

        return false;
    }

    private static string SanitizeFileName(string? name, string fallback)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            return fallback;
        }

        var sanitized = Regex.Replace(name, "[\\r\\n\\0\"\\\\]", "_").Trim();
        return string.IsNullOrWhiteSpace(sanitized)
            ? fallback
            : sanitized[..Math.Min(200, sanitized.Length)];
    }

    private static string? ExtractFileName(ContentDispositionHeaderValue? contentDisposition)
        => contentDisposition?.FileNameStar ?? contentDisposition?.FileName?.Trim('"');

    private static GlSession ParseAuthSession(string body, string fallbackUsername)
    {
        using var document = JsonDocument.Parse(body);
        var root = document.RootElement;

        var id = GetString(root, "id")
            ?? throw new GlobalLeaksValidationException("GlobalLeaks no devolvió una sesión válida.", 502);
        var username = GetString(root, "username", "name");
        if (string.IsNullOrWhiteSpace(username))
        {
            username = fallbackUsername.Trim();
        }

        var role = GetString(root, "role", "user_role", "userRole");

        return new GlSession(id, username, role);
    }

    private static async Task EnsureSuccessOrThrowAsync(
        HttpResponseMessage response,
        string endpoint,
        CancellationToken cancellationToken)
    {
        if (response.IsSuccessStatusCode)
        {
            return;
        }

        var body = await ReadBodySafeAsync(response, cancellationToken);
        throw new GlobalLeaksValidationException(
            string.IsNullOrWhiteSpace(body)
                ? $"Error en {endpoint} (código {(int)response.StatusCode})."
                : $"Error en {endpoint} (código {(int)response.StatusCode}): {body}",
            (int)response.StatusCode);
    }

    private static async Task<string> ReadBodySafeAsync(HttpResponseMessage response, CancellationToken cancellationToken)
    {
        try
        {
            return (await response.Content.ReadAsStringAsync(cancellationToken)).Trim();
        }
        catch
        {
            return string.Empty;
        }
    }

    private static bool IsTwoFactorRequiredError(string body)
    {
        if (string.IsNullOrWhiteSpace(body))
        {
            return false;
        }

        return body.Contains("\"error_code\":13", StringComparison.OrdinalIgnoreCase) ||
               body.Contains("\"error_code\": 13", StringComparison.OrdinalIgnoreCase) ||
               body.Contains("Two Factor authentication required", StringComparison.OrdinalIgnoreCase);
    }

    private static bool IsMissingTokenOrSessionError(string body)
    {
        return !string.IsNullOrWhiteSpace(body) &&
               (body.Contains("No token and no session", StringComparison.OrdinalIgnoreCase) ||
                body.Contains("Invalid request: No token", StringComparison.OrdinalIgnoreCase));
    }

    private sealed record TokenResponse(string Id, string Salt);
    private sealed record AuthTypeResponse(string Type, string Salt);

    private sealed record RawReport
    {
        public string Id { get; init; } = string.Empty;
        public int? Progressive { get; init; }
        public string? ContextId { get; init; }
        public string? CreationDate { get; init; }
        public string? UpdateDate { get; init; }
        public string? ExpirationDate { get; init; }
        public string? ReminderDate { get; init; }
        public string? AccessDate { get; init; }
        public string? LastAccess { get; init; }
        public string? Status { get; init; }
        public bool Updated { get; init; }
        public string? Label { get; init; }
    }
}
