using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Cookies;

namespace ZB.MOM.WW.ScadaBridge.Security;

/// <summary>
/// The single, shared source of truth for the FULL set of claims that
/// back an interactive cookie session. BOTH the <c>/auth/login</c> endpoint and the
/// <c>OnValidatePrincipal</c> mid-session role-refresh path build their principal
/// through <see cref="Build"/>, so the two can never drift — the spec requires the
/// refresh to "rebuild claims identically to /auth/login".
/// </summary>
/// <remarks>
/// The claim shape is exactly what the login endpoint historically minted, plus the
/// two additions:
/// <list type="bullet">
///   <item><see cref="ClaimTypes.Name"/> — resolves <c>Identity.Name</c>.</item>
///   <item><see cref="JwtTokenService.DisplayNameClaimType"/> — human display name.</item>
///   <item><see cref="JwtTokenService.UsernameClaimType"/> — canonical username.</item>
///   <item><see cref="JwtTokenService.RoleClaimType"/> — one per mapped role.</item>
///   <item><see cref="JwtTokenService.SiteIdClaimType"/> — one per permitted site,
///         ONLY when the mapping is not system-wide (deny-by-omission preserved).</item>
///   <item><see cref="JwtTokenService.GroupClaimType"/> — one per raw LDAP group
///         — the durable input the mid-session RoleMapper re-run consumes.</item>
///   <item><see cref="JwtTokenService.LastRoleRefreshClaimType"/> — the role-mapping
///         refresh anchor, ISO-8601 round-trippable.</item>
///   <item><see cref="JwtTokenService.LastActivityClaimType"/> — the idle-timeout
///         anchor; seeded to the refresh timestamp at login so idle-timeout can be
///         enforced consistently from the very first request.</item>
/// </list>
/// The <see cref="ClaimsIdentity"/> is built with <c>nameType = ClaimTypes.Name</c>
/// and <c>roleType = RoleClaimType</c> so <c>Identity.Name</c> / <c>IsInRole</c> /
/// <c>[Authorize(Roles=…)]</c> resolve against exactly the canonical types minted here.
/// </remarks>
public static class SessionClaimBuilder
{
    /// <summary>
    /// Builds the full cookie-session <see cref="ClaimsPrincipal"/> from the resolved
    /// identity, the raw LDAP groups, the DB-backed role mapping, and the refresh
    /// timestamp. Used identically by <c>/auth/login</c> and the
    /// <c>OnValidatePrincipal</c> refresh path so the two cannot diverge.
    /// </summary>
    /// <param name="username">The canonical authenticated username (becomes <see cref="ClaimTypes.Name"/> + <see cref="JwtTokenService.UsernameClaimType"/>).</param>
    /// <param name="displayName">The human-readable display name.</param>
    /// <param name="groups">The user's raw LDAP groups, stored one per <see cref="JwtTokenService.GroupClaimType"/> claim.</param>
    /// <param name="mapping">The DB-backed role mapping (roles + permitted sites + system-wide flag).</param>
    /// <param name="refreshTimestamp">The role-mapping refresh anchor; also seeds the last-activity anchor.</param>
    /// <param name="authenticationType">The authentication type stamped on the identity (defaults to the cookie scheme).</param>
    /// <returns>A fully populated cookie <see cref="ClaimsPrincipal"/>.</returns>
    public static ClaimsPrincipal Build(
        string username,
        string displayName,
        IReadOnlyList<string> groups,
        RoleMappingResult mapping,
        DateTimeOffset refreshTimestamp,
        string authenticationType = CookieAuthenticationDefaults.AuthenticationScheme)
    {
        ArgumentNullException.ThrowIfNull(username);
        ArgumentNullException.ThrowIfNull(displayName);
        ArgumentNullException.ThrowIfNull(groups);
        ArgumentNullException.ThrowIfNull(mapping);

        var refreshStamp = refreshTimestamp.ToString("o");

        var claims = new List<Claim>
        {
            new(ClaimTypes.Name, username),
            new(JwtTokenService.DisplayNameClaimType, displayName),
            new(JwtTokenService.UsernameClaimType, username),
            // Role-refresh anchor AND idle anchor are seeded from the same instant at
            // build time. They then diverge: OnValidatePrincipal advances LastActivity
            // on every request but only advances LastRoleRefresh when it actually
            // re-runs the mapping.
            new(JwtTokenService.LastRoleRefreshClaimType, refreshStamp),
            new(JwtTokenService.LastActivityClaimType, refreshStamp),
        };

        foreach (var role in mapping.Roles)
        {
            claims.Add(new Claim(JwtTokenService.RoleClaimType, role));
        }

        // Deny-by-omission: only stamp SiteId claims for a non-system-wide mapping.
        if (!mapping.IsSystemWideDeployment)
        {
            foreach (var siteId in mapping.PermittedSiteIds)
            {
                claims.Add(new Claim(JwtTokenService.SiteIdClaimType, siteId));
            }
        }

        // Store the raw LDAP groups so the mid-session refresh can re-run the
        // DB-backed RoleMapper without any LDAP round-trip.
        foreach (var group in groups)
        {
            claims.Add(new Claim(JwtTokenService.GroupClaimType, group));
        }

        var identity = new ClaimsIdentity(
            claims,
            authenticationType: authenticationType,
            nameType: ClaimTypes.Name,
            roleType: JwtTokenService.RoleClaimType);

        return new ClaimsPrincipal(identity);
    }

    /// <summary>Reads the stored LDAP group claims (<see cref="JwtTokenService.GroupClaimType"/>) off a principal.</summary>
    /// <param name="principal">The cookie principal to read from.</param>
    /// <returns>The stored LDAP group names; empty if none were stored.</returns>
    public static IReadOnlyList<string> ReadGroups(ClaimsPrincipal principal)
    {
        ArgumentNullException.ThrowIfNull(principal);
        return principal.FindAll(JwtTokenService.GroupClaimType).Select(c => c.Value).ToList();
    }
}
