using System.Security.Claims;
using ApiOPE.Configuration;
using ApiOPE.Contracts;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;

namespace ApiOPE.Security;

public sealed class OpeAuthenticationMiddleware
{
    public const string TokenHeaderName = "X-Rest-Basic-Token";
    public const string OrganizationIdHeaderName = "X-Organization-ID";
    public const string OrganizationDir3HeaderName = "X-Organization-DIR3";
    public const string OrganizationCifHeaderName = "X-Organization-CIF";

    private readonly RequestDelegate _next;
    private readonly ILogger<OpeAuthenticationMiddleware> _logger;

    public OpeAuthenticationMiddleware(
        RequestDelegate next,
        ILogger<OpeAuthenticationMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(
        HttpContext context,
        OpeTokenValidator tokenValidator,
        OpeResponseSigner responseSigner,
        IOptions<OpeOptions> options)
    {
        var authentication = context.GetEndpoint()?.Metadata.GetMetadata<OpeAuthenticatedAttribute>();
        if (authentication is null)
        {
            await _next(context);
            return;
        }

        if (!TryGetSingleHeader(context.Request.Headers, TokenHeaderName, out var token))
        {
            await RejectAsync(context, "Falta la cabecera de autenticacion OPE.");
            return;
        }

        var validation = tokenValidator.Validate(token, authentication.Resource, authentication.VersionOptional);
        if (!validation.IsValid || validation.RequestContext is null)
        {
            await RejectAsync(context, validation.FailureReason);
            return;
        }

        if (!ValidateOrganizationHeaders(context.Request.Headers, options.Value, out var organizationFailure))
        {
            await RejectAsync(context, organizationFailure);
            return;
        }

        OpeRequestContextStore.Set(context, validation.RequestContext);
        context.User = new ClaimsPrincipal(new ClaimsIdentity(
        [
            new Claim("client_token", validation.RequestContext.ClientToken),
            new Claim("transaction_id", validation.RequestContext.TransactionId.ToString()),
            new Claim("uuid", validation.RequestContext.Uuid.ToString())
        ], "OpeJwt"));

        context.Response.OnStarting(() =>
        {
            context.Response.Headers["Signature"] = responseSigner.Sign(validation.RequestContext.Uuid);
            context.Response.Headers.CacheControl = "no-store";
            context.Response.Headers.Pragma = "no-cache";
            context.Response.Headers["X-Content-Type-Options"] = "nosniff";
            return Task.CompletedTask;
        });

        await _next(context);
    }

    private async Task RejectAsync(HttpContext context, string reason)
    {
        _logger.LogWarning(
            "Peticion OPE rechazada en {Method} {Path}. Motivo={Reason}",
            context.Request.Method,
            context.Request.Path,
            reason);

        await OpeResponseWriter.WriteErrorAsync(
            context.Response,
            StatusCodes.Status401Unauthorized,
            "Unauthorized",
            OpeErrorCauses.WrongSignature,
            null,
            context.RequestAborted);
    }

    private static bool ValidateOrganizationHeaders(
        IHeaderDictionary headers,
        OpeOptions options,
        out string failureReason)
    {
        var valid = MatchesHeader(headers, OrganizationIdHeaderName, options.OrganizationId) &&
                    MatchesHeader(headers, OrganizationDir3HeaderName, options.OrganizationDir3) &&
                    MatchesHeader(headers, OrganizationCifHeaderName, options.OrganizationCif);

        failureReason = valid
            ? string.Empty
            : "Las cabeceras de organizacion no corresponden al conector configurado.";
        return valid;
    }

    private static bool MatchesHeader(IHeaderDictionary headers, string name, string expected)
    {
        return TryGetSingleHeader(headers, name, out var value) &&
               string.Equals(value, expected, StringComparison.OrdinalIgnoreCase);
    }

    private static bool TryGetSingleHeader(
        IHeaderDictionary headers,
        string name,
        out string value)
    {
        value = string.Empty;
        if (!headers.TryGetValue(name, out StringValues values) || values.Count != 1)
        {
            return false;
        }

        value = values[0]?.Trim() ?? string.Empty;
        return !string.IsNullOrWhiteSpace(value);
    }
}
