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

/// <summary>
/// Single source of truth for mapping a user's LDAP groups to dashboard roles.
/// Both <see cref="DashboardAuthenticator"/> (the existing login flow) and
/// <see cref="DashboardGroupRoleMapper"/> (the shared-Auth
/// <see cref="ZB.MOM.WW.Auth.Abstractions.Roles.IGroupRoleMapper{TRole}"/> seam)
/// delegate here so the precedence and case rules stay identical.
/// </summary>
internal static class DashboardGroupRoleMapping
{
    /// <summary>
    /// Maps the user's LDAP groups to dashboard roles. A user can pick up
    /// multiple roles; Admin and Viewer are the only legal values. Returns
    /// an empty list when no group matches (caller rejects the login).
    /// </summary>
    /// <param name="groups">The collection of LDAP groups the user belongs to.</param>
    /// <param name="groupToRole">The mapping from group names to dashboard role names.</param>
    /// <returns>The distinct dashboard roles the groups map to, or an empty list if none match.</returns>
    internal static IReadOnlyList<string> MapGroupsToRoles(
        IEnumerable<string> groups,
        IReadOnlyDictionary<string, string> groupToRole)
    {
        if (groupToRole.Count == 0)
        {
            return [];
        }

        HashSet<string> roles = new(StringComparer.Ordinal);
        foreach (string group in groups)
        {
            string normalizedGroup = group.Trim();

            // Lookup precedence: the full literal group string is tried first; only if
            // that misses do we fall back to the leading RDN value (e.g. "GwAdmin"
            // extracted from "ou=GwAdmin,ou=groups,..."). The map's comparer is
            // OrdinalIgnoreCase (see DashboardOptions.GroupToRole), so "GwAdmin" and
            // "gwadmin" both match.
            //
            // With the shared ZB.MOM.WW.Auth.Ldap provider, groups arrive here already
            // stripped to short RDN names (the library calls FirstRdnValue before
            // returning them). So through the live login path the full-string branch
            // only ever sees short names and the RDN fallback is effectively a no-op —
            // they collapse to the same key. The fallback is retained because this
            // mapping is also reachable directly via the IGroupRoleMapper<string> seam
            // (DashboardGroupRoleMapper), where a caller could still pass a full DN.
            // CONSEQUENCE: configuring a full-DN GroupToRole *key* (e.g.
            // "ou=GwAdmin,ou=groups,...") is UNSUPPORTED with the shared library — the
            // incoming group is a short name, so it will never equal a full-DN key.
            // Keep GroupToRole keys as short group names.
            if (groupToRole.TryGetValue(normalizedGroup, out string? mapped)
                || groupToRole.TryGetValue(ExtractFirstRdnValue(normalizedGroup), out mapped))
            {
                roles.Add(mapped);
            }
        }

        return [.. roles];
    }

    /// <summary>Extracts the first RDN value from a distinguished name.</summary>
    /// <param name="distinguishedName">The LDAP distinguished name.</param>
    /// <returns>The value of the first RDN component, or the input unchanged if it contains no '=' separator.</returns>
    internal static string ExtractFirstRdnValue(string distinguishedName)
    {
        int equalsIndex = distinguishedName.IndexOf('=');
        if (equalsIndex < 0)
        {
            return distinguishedName;
        }

        int valueStart = equalsIndex + 1;
        int commaIndex = distinguishedName.IndexOf(',', valueStart);

        return commaIndex > valueStart
            ? distinguishedName[valueStart..commaIndex]
            : distinguishedName[valueStart..];
    }
}
