using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Auth.Abstractions;
using StellaOps.Feedser.WebService.Options;

namespace StellaOps.Feedser.WebService.Filters;

/// <summary>
/// Emits structured audit logs for job endpoint authorization decisions, including bypass usage.
/// </summary>
public sealed class JobAuthorizationAuditFilter : IEndpointFilter
{
    internal const string LoggerName = "Feedser.Authorization.Audit";

    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
    {
        ArgumentNullException.ThrowIfNull(context);
        ArgumentNullException.ThrowIfNull(next);

        var httpContext = context.HttpContext;
        var options = httpContext.RequestServices.GetRequiredService<IOptions<FeedserOptions>>().Value;
        var authority = options.Authority;

        if (authority is null || !authority.Enabled)
        {
            return await next(context).ConfigureAwait(false);
        }

        var logger = httpContext.RequestServices
            .GetRequiredService<ILoggerFactory>()
            .CreateLogger(LoggerName);

        var remoteAddress = httpContext.Connection.RemoteIpAddress;
        var matcher = new NetworkMaskMatcher(authority.BypassNetworks);
        var user = httpContext.User;
        var isAuthenticated = user?.Identity?.IsAuthenticated ?? false;
        var bypassUsed = !isAuthenticated && matcher.IsAllowed(remoteAddress);

        var result = await next(context).ConfigureAwait(false);

        var scopes = ExtractScopes(user);
        var subject = user?.FindFirst(StellaOpsClaimTypes.Subject)?.Value;
        var clientId = user?.FindFirst(StellaOpsClaimTypes.ClientId)?.Value;

        logger.LogInformation(
            "Feedser authorization audit route={Route} status={StatusCode} subject={Subject} clientId={ClientId} scopes={Scopes} bypass={Bypass} remote={RemoteAddress}",
            httpContext.Request.Path.Value ?? string.Empty,
            httpContext.Response.StatusCode,
            string.IsNullOrWhiteSpace(subject) ? "(anonymous)" : subject,
            string.IsNullOrWhiteSpace(clientId) ? "(none)" : clientId,
            scopes.Length == 0 ? "(none)" : string.Join(',', scopes),
            bypassUsed,
            remoteAddress?.ToString() ?? IPAddress.None.ToString());

        return result;
    }

    private static string[] ExtractScopes(ClaimsPrincipal? principal)
    {
        if (principal is null)
        {
            return Array.Empty<string>();
        }

        var values = new HashSet<string>(StringComparer.Ordinal);

        foreach (var claim in principal.FindAll(StellaOpsClaimTypes.ScopeItem))
        {
            if (string.IsNullOrWhiteSpace(claim.Value))
            {
                continue;
            }

            values.Add(claim.Value);
        }

        foreach (var claim in principal.FindAll(StellaOpsClaimTypes.Scope))
        {
            if (string.IsNullOrWhiteSpace(claim.Value))
            {
                continue;
            }

            var parts = claim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
            foreach (var part in parts)
            {
                var normalized = StellaOpsScopes.Normalize(part);
                if (!string.IsNullOrEmpty(normalized))
                {
                    values.Add(normalized);
                }
            }
        }

        return values.Count == 0 ? Array.Empty<string>() : values.ToArray();
    }
}
