namespace ZB.MOM.WW.Auth.Ldap.Internal;

/// <summary>
/// RFC 4515 LDAP filter escaping and RFC 4514 DN attribute-value escaping utilities.
/// </summary>
internal static class LdapEscaping
{
    /// <summary>
    /// Escapes a string for safe use inside an RFC 4515 LDAP search filter assertion value.
    /// Escapes (in order): backslash, asterisk, left-paren, right-paren, NUL.
    /// </summary>
    public static string Filter(string input)
    {
        if (string.IsNullOrEmpty(input))
            return input;

        // Backslash must be escaped first so we don't double-escape subsequent replacements.
        return input
            .Replace("\\", @"\5c")
            .Replace("*",  @"\2a")
            .Replace("(",  @"\28")
            .Replace(")",  @"\29")
            .Replace("\0", @"\00");
    }

    /// <summary>
    /// Escapes a string for safe use as an RFC 4514 DN attribute value.
    /// Escapes: , + " \ &lt; &gt; ; (with a leading backslash);
    /// also escapes a leading '#' and leading/trailing space.
    /// </summary>
    public static string Dn(string input)
    {
        if (string.IsNullOrEmpty(input))
            return input;

        var sb = new System.Text.StringBuilder(input.Length + 8);
        for (var i = 0; i < input.Length; i++)
        {
            var c = input[i];
            switch (c)
            {
                case ',':
                case '+':
                case '"':
                case '\\':
                case '<':
                case '>':
                case ';':
                    sb.Append('\\').Append(c);
                    break;
                case '#' when i == 0:
                    sb.Append(@"\#");
                    break;
                case ' ' when i == 0 || i == input.Length - 1:
                    sb.Append(@"\ ");
                    break;
                default:
                    sb.Append(c);
                    break;
            }
        }
        return sb.ToString();
    }

    /// <summary>
    /// Extracts the value of the first RDN from a DN, e.g.
    /// <c>cn=Engineers,ou=g,dc=x</c> → <c>Engineers</c>. The scan is RFC 4514 escape-aware:
    /// a backslash-escaped <c>,</c> inside the RDN value does not terminate it, and recognised
    /// escape sequences — single-character (<c>\,</c> <c>\\</c> …) and two-digit hex
    /// (<c>\2c</c>) — are unescaped, so a group CN that legitimately contains a comma is
    /// returned intact (Security-013). A string with no <c>=</c> is returned unchanged.
    /// </summary>
    public static string FirstRdnValue(string dn)
    {
        if (string.IsNullOrEmpty(dn))
            return dn;

        var equalsIndex = dn.IndexOf('=');
        if (equalsIndex < 0)
            return dn;

        var valueStart = equalsIndex + 1;
        var sb = new System.Text.StringBuilder(dn.Length - valueStart);

        for (var i = valueStart; i < dn.Length; i++)
        {
            var c = dn[i];
            if (c == '\\' && i + 1 < dn.Length)
            {
                var next = dn[i + 1];
                // RFC 4514 hex escape: \XX (two hex digits).
                if (i + 2 < dn.Length && IsHexDigit(next) && IsHexDigit(dn[i + 2]))
                {
                    sb.Append((char)Convert.ToInt32(dn.Substring(i + 1, 2), 16));
                    i += 2;
                }
                else
                {
                    // Single-character escape (e.g. \, \+ \\ \" \; etc.) — emit the
                    // escaped character literally and skip the backslash.
                    sb.Append(next);
                    i += 1;
                }
                continue;
            }

            if (c == ',')
            {
                // Unescaped comma terminates the first RDN.
                break;
            }

            sb.Append(c);
        }

        return sb.ToString();
    }

    private static bool IsHexDigit(char c)
        => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
