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 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)
    {
        using var tokenRequest = CreateRequest(HttpMethod.Post, "/api/auth/token");
        using var tokenResponse = await _httpClient.SendAsync(tokenRequest, 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 criptográfico.", 502);

        var tokenAnswer = SolveProofOfWork(tokenData.Id, tokenData.Salt);

        using var typeRequest = CreateRequest(HttpMethod.Post, "/api/auth/type");
        typeRequest.Content = CreateJsonContent(new { username });
        using var typeResponse = await _httpClient.SendAsync(typeRequest, 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 finalPassword = authType.Type == "key"
            ? DerivePassword(password, authType.Salt)
            : password;

        using var authRequest = CreateRequest(HttpMethod.Post, "/api/auth/authentication");
        authRequest.Content = CreateJsonContent(new
        {
            tid = 1,
            username,
            password = finalPassword,
            authcode,
        });
        authRequest.Headers.Add("X-Token", tokenAnswer);

        using var authResponse = await _httpClient.SendAsync(authRequest, cancellationToken);
        if (!authResponse.IsSuccessStatusCode)
        {
            var body = await ReadBodySafeAsync(authResponse, cancellationToken);
            throw authResponse.StatusCode switch
            {
                HttpStatusCode.Unauthorized => new GlobalLeaksValidationException(
                    "Credenciales incorrectas o código 2FA inválido.",
                    StatusCodes.Status401Unauthorized),
                (HttpStatusCode)429 => new GlobalLeaksValidationException(
                    "Demasiados intentos en GlobalLeaks. Espera unos minutos.",
                    StatusCodes.Status429TooManyRequests),
                _ => new GlobalLeaksValidationException(
                    string.IsNullOrWhiteSpace(body)
                        ? $"Login fallido (código {(int)authResponse.StatusCode})."
                        : $"Login fallido (código {(int)authResponse.StatusCode}): {body}",
                    (int)authResponse.StatusCode),
            };
        }

        var authBody = await authResponse.Content.ReadAsStringAsync(cancellationToken);
        var session = ParseAuthSession(authBody, username);
        _logger.LogInformation("Login GlobalLeaks correcto para {Username}. Rol: {Role}", session.Username, session.Role ?? "(sin rol)");
        return session;
    }

    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)
    {
        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 contexts = await GetContextsAsync(sessionId, cancellationToken);
        var contextNames = contexts.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<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> 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)
    {
        var idBytes = Encoding.UTF8.GetBytes(tokenId);
        var saltBytes = Convert.FromBase64String(tokenSalt).Take(16).ToArray();
        var n = 0;

        while (true)
        {
            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 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 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 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 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; }
    }
}
