using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using ApiOPE.Configuration;
using Microsoft.Extensions.Options;

namespace ApiOPE.Security;

public sealed record OpeTokenValidationResult(
    bool IsValid,
    OpeRequestContext? RequestContext,
    string FailureReason)
{
    public static OpeTokenValidationResult Success(OpeRequestContext context)
        => new(true, context, string.Empty);

    public static OpeTokenValidationResult Failure(string reason)
        => new(false, null, reason);
}

public sealed class OpeTokenValidator
{
    private const int MaxTokenLength = 16 * 1024;

    private readonly OpeOptions _options;
    private readonly TimeProvider _timeProvider;
    private readonly byte[] _secretKey;

    public OpeTokenValidator(IOptions<OpeOptions> options, TimeProvider timeProvider)
    {
        _options = options.Value;
        _timeProvider = timeProvider;
        _secretKey = Encoding.UTF8.GetBytes(_options.SecretKey);
    }

    public OpeTokenValidationResult Validate(
        string token,
        string expectedResource,
        bool versionOptional)
    {
        if (string.IsNullOrWhiteSpace(token) || token.Length > MaxTokenLength)
        {
            return OpeTokenValidationResult.Failure("Token ausente o demasiado largo.");
        }

        var segments = token.Split('.');
        if (segments.Length != 3 || segments.Any(string.IsNullOrWhiteSpace))
        {
            return OpeTokenValidationResult.Failure("El JWT no contiene tres segmentos.");
        }

        byte[] headerBytes;
        byte[] payloadBytes;
        byte[] receivedSignature;
        try
        {
            headerBytes = DecodeBase64Url(segments[0]);
            payloadBytes = DecodeBase64Url(segments[1]);
            receivedSignature = DecodeBase64Url(segments[2]);
        }
        catch (FormatException)
        {
            return OpeTokenValidationResult.Failure("El JWT no esta codificado en Base64Url valido.");
        }

        if (!HasSupportedHeader(headerBytes))
        {
            return OpeTokenValidationResult.Failure("El JWT no declara exclusivamente HS256.");
        }

        var signedBytes = Encoding.ASCII.GetBytes($"{segments[0]}.{segments[1]}");
        var expectedSignature = HMACSHA256.HashData(_secretKey, signedBytes);
        if (receivedSignature.Length != expectedSignature.Length ||
            !CryptographicOperations.FixedTimeEquals(receivedSignature, expectedSignature))
        {
            return OpeTokenValidationResult.Failure("La firma del JWT no es valida.");
        }

        try
        {
            using var payload = JsonDocument.Parse(payloadBytes, new JsonDocumentOptions
            {
                AllowTrailingCommas = false,
                CommentHandling = JsonCommentHandling.Disallow,
                MaxDepth = 8
            });

            if (payload.RootElement.ValueKind != JsonValueKind.Object ||
                HasDuplicateProperties(payload.RootElement))
            {
                return OpeTokenValidationResult.Failure("El payload del JWT no es un objeto valido.");
            }

            var clientToken = GetRequiredString(payload.RootElement, "client_token");
            var resource = GetRequiredString(payload.RootElement, "resource");
            var uuidText = GetRequiredString(payload.RootElement, "uuid");
            var transactionIdText = GetRequiredString(payload.RootElement, "transaction_id");
            var version = GetOptionalString(payload.RootElement, "version");
            var timestamp = GetRequiredInt64(payload.RootElement, "timestamp");

            if (!FixedTimeEquals(clientToken, _options.ClientToken))
            {
                return OpeTokenValidationResult.Failure("El client_token no corresponde al conector.");
            }

            if (!string.Equals(resource, expectedResource, StringComparison.Ordinal))
            {
                return OpeTokenValidationResult.Failure("El recurso firmado no corresponde al recurso solicitado.");
            }

            if ((!versionOptional && !string.Equals(version, _options.Version, StringComparison.Ordinal)) ||
                (versionOptional && version is not null && !string.Equals(version, _options.Version, StringComparison.Ordinal)))
            {
                return OpeTokenValidationResult.Failure("La version firmada no es compatible.");
            }

            if (!Guid.TryParseExact(uuidText, "D", out _) ||
                !Guid.TryParseExact(transactionIdText, "D", out var transactionId))
            {
                return OpeTokenValidationResult.Failure("uuid o transaction_id no tienen formato UUID.");
            }

            var now = _timeProvider.GetUtcNow().ToUnixTimeSeconds();
            if (timestamp > now + _options.ClockSkewSeconds ||
                timestamp < now - _options.TokenMaxAgeSeconds - _options.ClockSkewSeconds)
            {
                return OpeTokenValidationResult.Failure("El timestamp del JWT esta fuera de la ventana permitida.");
            }

            return OpeTokenValidationResult.Success(new OpeRequestContext(
                clientToken,
                resource,
                uuidText,
                transactionId,
                timestamp,
                version));
        }
        catch (JsonException)
        {
            return OpeTokenValidationResult.Failure("El payload del JWT no contiene JSON valido.");
        }
        catch (InvalidOperationException exception)
        {
            return OpeTokenValidationResult.Failure(exception.Message);
        }
    }

    private static bool HasSupportedHeader(byte[] headerBytes)
    {
        try
        {
            using var header = JsonDocument.Parse(headerBytes, new JsonDocumentOptions
            {
                AllowTrailingCommas = false,
                CommentHandling = JsonCommentHandling.Disallow,
                MaxDepth = 4
            });

            return header.RootElement.ValueKind == JsonValueKind.Object &&
                   !HasDuplicateProperties(header.RootElement) &&
                   string.Equals(GetRequiredString(header.RootElement, "alg"), "HS256", StringComparison.Ordinal);
        }
        catch (JsonException)
        {
            return false;
        }
        catch (InvalidOperationException)
        {
            return false;
        }
    }

    private static byte[] DecodeBase64Url(string value)
    {
        var base64 = value.Replace('-', '+').Replace('_', '/');
        base64 = (base64.Length % 4) switch
        {
            0 => base64,
            2 => base64 + "==",
            3 => base64 + "=",
            _ => throw new FormatException("Longitud Base64Url no valida.")
        };

        return Convert.FromBase64String(base64);
    }

    private static bool HasDuplicateProperties(JsonElement element)
    {
        var names = new HashSet<string>(StringComparer.Ordinal);
        return element.EnumerateObject().Any(property => !names.Add(property.Name));
    }

    private static string GetRequiredString(JsonElement element, string name)
    {
        if (!element.TryGetProperty(name, out var property) ||
            property.ValueKind != JsonValueKind.String ||
            string.IsNullOrWhiteSpace(property.GetString()))
        {
            throw new InvalidOperationException($"Falta la claim obligatoria {name}.");
        }

        return property.GetString()!;
    }

    private static string? GetOptionalString(JsonElement element, string name)
    {
        if (!element.TryGetProperty(name, out var property))
        {
            return null;
        }

        if (property.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(property.GetString()))
        {
            throw new InvalidOperationException($"La claim {name} tiene un formato incorrecto.");
        }

        return property.GetString();
    }

    private static long GetRequiredInt64(JsonElement element, string name)
    {
        if (!element.TryGetProperty(name, out var property) ||
            property.ValueKind != JsonValueKind.Number ||
            !property.TryGetInt64(out var value))
        {
            throw new InvalidOperationException($"Falta la claim numerica obligatoria {name}.");
        }

        return value;
    }

    private static bool FixedTimeEquals(string left, string right)
    {
        var leftHash = SHA256.HashData(Encoding.UTF8.GetBytes(left));
        var rightHash = SHA256.HashData(Encoding.UTF8.GetBytes(right));
        return CryptographicOperations.FixedTimeEquals(leftHash, rightHash);
    }
}
