using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using ZB.MOM.WW.Auth.AspNetCore;

namespace ZB.MOM.WW.OtOpcUa.Security.Jwt;

public sealed class JwtTokenService
{
    /// <summary>
    /// Alias of <see cref="ZbClaimTypes.DisplayName"/> — the canonical "zb:displayname" claim.
    /// All read and mint sites inherit the canonical spelling through this constant.
    /// </summary>
    public const string DisplayNameClaimType = ZbClaimTypes.DisplayName;

    /// <summary>
    /// Alias of <see cref="ZbClaimTypes.Username"/> — the canonical "zb:username" claim.
    /// All read and mint sites inherit the canonical spelling through this constant.
    /// </summary>
    public const string UsernameClaimType = ZbClaimTypes.Username;

    /// <summary>
    /// Role claim type used in the JWT payload.
    /// <para>
    /// <b>Issued-only / no internal JwtBearer scheme:</b> OtOpcUa uses a single Cookie
    /// authentication scheme; the JWT is minted by the <c>/auth/token</c> endpoint and
    /// consumed externally (e.g. by OPC-UA clients or automation scripts). There is no
    /// <c>AddJwtBearer</c> pipeline in OtOpcUa — the cookie stores the
    /// <see cref="System.Security.Claims.ClaimsPrincipal"/> directly. Because no internal
    /// bearer validation path exists, the short "Role" key is intentionally used here rather
    /// than the long <see cref="ClaimTypes.Role"/> URI; external consumers receive exactly the
    /// key they expect.
    /// </para>
    /// <para>
    /// <b>If a JwtBearer scheme is ever added:</b> the
    /// <see cref="Microsoft.IdentityModel.Tokens.TokenValidationParameters"/> passed to
    /// <c>AddJwtBearer</c> MUST set <c>RoleClaimType = JwtTokenService.RoleClaimType</c> (and
    /// <c>NameClaimType = JwtTokenService.UsernameClaimType</c>) so that
    /// <c>[Authorize(Roles=...)]</c> and <c>ClaimsPrincipal.IsInRole</c> resolve correctly.
    /// <see cref="BuildValidationParameters"/> is already wired to do this and MUST be used
    /// rather than constructing <see cref="Microsoft.IdentityModel.Tokens.TokenValidationParameters"/>
    /// ad hoc.
    /// </para>
    /// </summary>
    public const string RoleClaimType = "Role";

    private readonly JwtOptions _options;
    private readonly ILogger<JwtTokenService> _logger;

    /// <summary>Initializes a new instance of the JwtTokenService class.</summary>
    /// <param name="options">The JWT options.</param>
    /// <param name="logger">The logger for the service.</param>
    public JwtTokenService(IOptions<JwtOptions> options, ILogger<JwtTokenService> logger)
    {
        _options = options.Value;
        _logger = logger;

        var keyByteLength = string.IsNullOrEmpty(_options.SigningKey)
            ? 0
            : Encoding.UTF8.GetByteCount(_options.SigningKey);
        if (keyByteLength < JwtOptions.MinSigningKeyBytes)
        {
            throw new InvalidOperationException(
                $"JwtOptions.SigningKey must be at least {JwtOptions.MinSigningKeyBytes} bytes " +
                $"(256 bits) for HMAC-SHA256; the configured key is {keyByteLength} byte(s).");
        }
    }

    /// <summary>Issues a JWT token for the specified user with the given roles.</summary>
    /// <param name="displayName">The display name of the user.</param>
    /// <param name="username">The username of the user.</param>
    /// <param name="roles">The roles assigned to the user.</param>
    /// <returns>The JWT token string.</returns>
    public string Issue(string displayName, string username, IReadOnlyList<string> roles)
    {
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.SigningKey));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var claims = new List<Claim>
        {
            new(DisplayNameClaimType, displayName),
            new(UsernameClaimType, username),
        };
        // Role claims use the short RoleClaimType key ("Role") — see the <see cref="RoleClaimType"/>
        // doc comment for the issued-only rationale and the JwtBearer caveat.
        foreach (var role in roles)
            claims.Add(new Claim(RoleClaimType, role));

        var token = new JwtSecurityToken(
            issuer: _options.Issuer,
            audience: _options.Audience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(_options.ExpiryMinutes),
            signingCredentials: credentials);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    /// <summary>Attempts to validate a JWT token and extract the claims principal.</summary>
    /// <param name="token">The JWT token to validate.</param>
    /// <param name="principal">The claims principal extracted from the token, or null if validation failed.</param>
    /// <returns>True if the token is valid; otherwise false.</returns>
    public bool TryValidate(string token, out ClaimsPrincipal? principal)
    {
        principal = null;
        // Delegate to BuildValidationParameters so RoleClaimType/NameClaimType are always in
        // sync with the mint constants — no risk of this method diverging from the bearer path.
        var parameters = BuildValidationParameters();

        try
        {
            var handler = new JwtSecurityTokenHandler();
            principal = handler.ValidateToken(token, parameters, out _);
            return true;
        }
        catch (Exception ex) when (ex is SecurityTokenException or ArgumentException)
        {
            _logger.LogDebug(ex, "JWT validation failed");
            return false;
        }
    }

    /// <summary>
    /// Returns the validation parameters that the JwtBearer middleware should use. Centralised
    /// so the bearer pipeline can't drift from <see cref="TryValidate"/>.
    /// <para>
    /// <b>Note:</b> <see cref="TokenValidationParameters.RoleClaimType"/> is set to
    /// <see cref="RoleClaimType"/> and <see cref="TokenValidationParameters.NameClaimType"/> is
    /// set to <see cref="UsernameClaimType"/> so that <c>[Authorize(Roles=...)]</c> and
    /// <c>ClaimsPrincipal.IsInRole</c> resolve against the short role key ("Role") that
    /// <see cref="Issue"/> mints — not the JWT-default "role" or "name" keys. This is the
    /// required pairing whenever a JwtBearer scheme is wired.
    /// </para>
    /// </summary>
    /// <returns>The <see cref="TokenValidationParameters"/> used to validate tokens minted by <see cref="Issue"/>.</returns>
    public TokenValidationParameters BuildValidationParameters() => new()
    {
        ValidateIssuer = true,
        ValidIssuer = _options.Issuer,
        ValidateAudience = true,
        ValidAudience = _options.Audience,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.SigningKey)),
        ClockSkew = TimeSpan.Zero,
        // Pair these with the constants used at mint time so role/name resolution is correct
        // if this is ever passed to AddJwtBearer. See RoleClaimType doc comment for rationale.
        RoleClaimType = RoleClaimType,
        NameClaimType = UsernameClaimType,
    };
}
