using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;

using Ablera.Serdica.Authentication.Constants;
using Ablera.Serdica.Authentication.Models;
using Ablera.Serdica.Authentication.Models.Oidc;
using Ablera.Serdica.Authority.Extensions;
using System.Net;

using OpenIddict.Validation.AspNetCore;
using Ablera.Serdica.Authentication.Extensions;

using static Ablera.Serdica.Authentication.Constants.ConstantsClass;

namespace Ablera.Serdica.Authentication.Services;

public sealed class SerdicaJwtBearerAuthenticationHandler : AuthenticationHandler<JwtBearerOptions>
{
    private readonly OidcValidation oidcValidationSettings;
    private readonly ILogger<SerdicaJwtBearerAuthenticationHandler> logger;

    public SerdicaJwtBearerAuthenticationHandler(
        IOptionsMonitor<JwtBearerOptions> jwtOptions,
        ILoggerFactory loggerFactory,
        ILogger<SerdicaJwtBearerAuthenticationHandler> logger,
        UrlEncoder encoder,
        IOptions<OidcValidation> oidcServerConnection)
        : base(jwtOptions, loggerFactory, encoder)
    {
        this.oidcValidationSettings = oidcServerConnection.Value;
        this.logger = logger;
    }
    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        // 1. Internal callers detected by bypass mask → Super user
        if (oidcValidationSettings.BypassValidationsMasks?
                .Any(m => m.MatchesRemote(Context)) == true)
        {
            return SuccessTicket(BuildDefaultRolePrincipal(SerdicaClaims.RoleSuperUser));
        }

        // 2. What roles does the endpoint require?
        Context.Items.TryGetValue(ConstantsClass.HttpContextEndpointRequiredRoles,
                                  out var rolesObj);
        var requiredRoles = rolesObj as string[];
        if (requiredRoles is { Length: 0 }) // empty means requirement for authentication claim
        {
            requiredRoles =
            [
                SerdicaClaims.IsAuthenticated
            ];
        }

        bool anonymousAllowed = requiredRoles == null ||
                                requiredRoles.Contains(SerdicaClaims.Anonymous,
                                                       StringComparer.Ordinal);

        // 3. Decide whether we *need* to run AuthenticateAsync
        bool tokenPresent =
            Context.Request.Headers.TryGetValue("Authorization", out var authHeaders) &&
            authHeaders.Any(h => h?.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) == true);

        bool mustAuthenticate = tokenPresent || !anonymousAllowed;

        AuthenticateResult authResult = mustAuthenticate
            ? await Context.AuthenticateAsync(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)
            : AuthenticateResult.NoResult();   // cheap placeholder; not succeeded, not failed
        logger.LogInformation(
            "Authorizing with following parameters authResult: {AuthResult}, anonymousAllowed: {anonymousAllowed}, tokenPresent: {tokenPresent}, requiredRoles: {requiredRoles}, roleClaims: {roleClaims}",
            authResult.Succeeded,
            anonymousAllowed,
            tokenPresent,
            string.Join(",", requiredRoles ?? []),
            string.Join(",", authResult?.Principal?.Claims?.Where(c => c.Type == ClaimTypes.Role)?.Select(c => c.Value) ?? [])
        );

        // 4. Figure out whether roles are satisfied (only matters if authenticated)
        bool rolesSatisfied = authResult?.Succeeded == true &&
                              !anonymousAllowed &&
                              requiredRoles is { Length: > 0 } &&
                              (requiredRoles.Contains(SerdicaClaims.IsAuthenticated) 
                                || 
                                (authResult?.Principal?.Claims
                                         ?.Where(c => c.Type == ClaimTypes.Role)
                                         ?.Select(c => c.Value)
                                         ?.Intersect(requiredRoles!)
                                         ?.Any() ?? false) == true);

        // 5. Switch expression drives the outcome
        return (anonymousAllowed, authResult?.Succeeded ?? false, rolesSatisfied) switch
        {
            // Anonymous endpoint
            (true, true, _) => SuccessTicket(authResult!.Principal!),              // token supplied
            (true, false, _) => SuccessTicket(BuildDefaultRolePrincipal(
                                   SerdicaClaims.Anonymous)),                       // no token

            // Protected endpoint but NOT authenticated
            (false, false, _) => AuthenticateResult.Fail(
                                    authResult!.Failure ?? new Exception("Token invalid.")),

            // Authenticated but lacks required roles
            (_, _, false) => AuthenticateResult.Fail("Insufficient privileges"),

            // Authenticated and authorised
            _ => SuccessTicket(authResult!.Principal!)
        };
    }

    protected override async Task HandleChallengeAsync(AuthenticationProperties props)
    {
        var proxy = new ProxyResult
        {
            StatusCode = HttpStatusCode.Unauthorized,               // 401
            TraceId = Context.TraceIdentifier,
            Title = "Unauthorized",
            Type = "https://datatracker.ietf.org/doc/html/rfc9110#section-15.5.2",
            Errors = new Dictionary<string, string>
            {
                ["authentication"] = "Missing or invalid credentials."
            }
        };
        await proxy.ReturnHttpRessponse(Response);
    }

    protected override async Task HandleForbiddenAsync(AuthenticationProperties props)
    {
        var proxy = new ProxyResult
        {
            StatusCode = HttpStatusCode.Forbidden,                  // 403
            TraceId = Context.TraceIdentifier,
            Title = "Forbidden",
            Type = "https://datatracker.ietf.org/doc/html/rfc9110#section-15.5.3",
            Errors = new Dictionary<string, string>
            {
                ["authorization"] = "Insufficient privileges."
            }
        };
        await proxy.ReturnHttpRessponse(Response);
    }

    // ──────────────────────────────────────────────────────────────────
    private ClaimsPrincipal BuildDefaultRolePrincipal(string role) => 
        PrincipalBuilder.Build($"{DefaultRolePrincipalPrefix}_{role}", ConstantsClass.AuthenticationScheme)
        .AddClaim(ClaimTypes.NameIdentifier, $"{DefaultRolePrincipalPrefix}_{role}")
        .AddClaim(ClaimTypes.Role, role);

    private static AuthenticateResult SuccessTicket(ClaimsPrincipal principal)
        => AuthenticateResult.Success(
                new AuthenticationTicket(
                    principal,
                    principal.Identity!.AuthenticationType!
                )
            );
}