using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;

namespace ZB.MOM.WW.MxGateway.Server.Dashboard;

/// <summary>
/// Authenticates SignalR hub connections by extracting a bearer token from
/// either the <c>Authorization</c> header or the <c>access_token</c> query
/// string (used by WebSocket upgrade requests that can't carry custom headers)
/// and validating it via <see cref="HubTokenService"/>.
/// </summary>
/// <remarks>
/// Carrying the token in the <c>?access_token=</c> query string is the standard SignalR pattern:
/// the WebSocket upgrade handshake cannot attach a custom <c>Authorization</c> header, so the JS
/// client appends the token to the URL. Because a query string is far easier to capture than a
/// header (proxy access logs, browser history, <c>Referer</c>), this value must NEVER be
/// request-logged. Serilog HTTP request logging is intentionally not enabled; if it is ever added,
/// scrub the <c>access_token</c> query parameter before the URL reaches a sink. The short token
/// lifetime (<see cref="HubTokenService.TokenLifetime"/>) is the backstop that bounds exposure of a
/// leaked token.
/// </remarks>
public sealed class HubTokenAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    private readonly HubTokenService _tokens;

    /// <summary>Initializes a new hub token authentication handler.</summary>
    /// <param name="options">The options monitor for the authentication scheme.</param>
    /// <param name="logger">The logger factory for logging authentication events.</param>
    /// <param name="encoder">The URL encoder for encoding authentication values.</param>
    /// <param name="tokens">The hub token service for validating tokens.</param>
    public HubTokenAuthenticationHandler(
        IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        HubTokenService tokens)
        : base(options, logger, encoder)
    {
        _tokens = tokens;
    }

    /// <inheritdoc />
    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        string? token = ExtractToken();
        if (string.IsNullOrEmpty(token))
        {
            return Task.FromResult(AuthenticateResult.NoResult());
        }

        System.Security.Claims.ClaimsPrincipal? principal = _tokens.Validate(token);
        if (principal is null)
        {
            return Task.FromResult(AuthenticateResult.Fail("Invalid or expired hub token."));
        }

        return Task.FromResult(AuthenticateResult.Success(
            new AuthenticationTicket(principal, Scheme.Name)));
    }

    private string? ExtractToken()
    {
        string header = Request.Headers.Authorization.ToString();
        if (header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
        {
            return header["Bearer ".Length..].Trim();
        }

        return Request.Query.TryGetValue("access_token", out Microsoft.Extensions.Primitives.StringValues queryToken)
            ? queryToken.ToString()
            : null;
    }
}
