using System.Collections.Generic;
using System.Globalization;
using System.Text;

namespace StellaOps.Feedser.Normalization.Identifiers;

/// <summary>
/// Implements canonical normalization for CPE 2.3 identifiers (and URI binding conversion).
/// </summary>
internal static class Cpe23
{
    private static readonly HashSet<char> CharactersRequiringEscape = new(new[]
    {
        '\\', ':', '/', '?', '#', '[', ']', '@', '!', '$', '&', '"', '\'', '(', ')', '+', ',', ';', '=', '%', '*',
        '<', '>', '|', '^', '`', '{', '}', '~'
    });

    public static bool TryNormalize(string? value, out string? normalized)
    {
        normalized = null;
        if (string.IsNullOrWhiteSpace(value))
        {
            return false;
        }

        var trimmed = value.Trim();
        var components = SplitComponents(trimmed);
        if (components.Count == 0)
        {
            return false;
        }

        if (!components[0].Equals("cpe", StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        if (components.Count >= 2 && components[1].Equals("2.3", StringComparison.OrdinalIgnoreCase))
        {
            return TryNormalizeFrom23(components, out normalized);
        }

        if (components.Count >= 2 && components[1].Length > 0 && components[1][0] == '/')
        {
            return TryNormalizeFrom22(components, out normalized);
        }

        return false;
    }

    private static bool TryNormalizeFrom23(IReadOnlyList<string> components, out string? normalized)
    {
        normalized = null;
        if (components.Count != 13)
        {
            return false;
        }

        var part = NormalizePart(components[2]);
        if (part is null)
        {
            return false;
        }

        var normalizedComponents = new string[13];
        normalizedComponents[0] = "cpe";
        normalizedComponents[1] = "2.3";
        normalizedComponents[2] = part;
        normalizedComponents[3] = NormalizeField(components[3], lower: true, decodeUri: false);
        normalizedComponents[4] = NormalizeField(components[4], lower: true, decodeUri: false);
        normalizedComponents[5] = NormalizeField(components[5], lower: false, decodeUri: false);
        normalizedComponents[6] = NormalizeField(components[6], lower: false, decodeUri: false);
        normalizedComponents[7] = NormalizeField(components[7], lower: false, decodeUri: false);
        normalizedComponents[8] = NormalizeField(components[8], lower: false, decodeUri: false);
        normalizedComponents[9] = NormalizeField(components[9], lower: false, decodeUri: false);
        normalizedComponents[10] = NormalizeField(components[10], lower: false, decodeUri: false);
        normalizedComponents[11] = NormalizeField(components[11], lower: false, decodeUri: false);
        normalizedComponents[12] = NormalizeField(components[12], lower: false, decodeUri: false);

        normalized = string.Join(':', normalizedComponents);
        return true;
    }

    private static bool TryNormalizeFrom22(IReadOnlyList<string> components, out string? normalized)
    {
        normalized = null;
        if (components.Count < 2)
        {
            return false;
        }

        var partComponent = components[1];
        if (partComponent.Length < 2 || partComponent[0] != '/')
        {
            return false;
        }

        var part = NormalizePart(partComponent[1..]);
        if (part is null)
        {
            return false;
        }

        var vendor = NormalizeField(components.Count > 2 ? components[2] : null, lower: true, decodeUri: true);
        var product = NormalizeField(components.Count > 3 ? components[3] : null, lower: true, decodeUri: true);
        var version = NormalizeField(components.Count > 4 ? components[4] : null, lower: false, decodeUri: true);
        var update = NormalizeField(components.Count > 5 ? components[5] : null, lower: false, decodeUri: true);

        var (edition, swEdition, targetSw, targetHw, other) = ExpandEdition(components.Count > 6 ? components[6] : null);
        var language = NormalizeField(components.Count > 7 ? components[7] : null, lower: true, decodeUri: true);

        normalized = string.Join(':', new[]
        {
            "cpe",
            "2.3",
            part,
            vendor,
            product,
            version,
            update,
            edition,
            language,
            swEdition,
            targetSw,
            targetHw,
            other,
        });

        return true;
    }

    private static string? NormalizePart(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        var token = value.Trim().ToLowerInvariant();
        return token is "a" or "o" or "h" ? token : null;
    }

    private static string NormalizeField(string? value, bool lower, bool decodeUri)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return "*";
        }

        var trimmed = value.Trim();
        if (trimmed is "*" or "-")
        {
            return trimmed;
        }

        var decoded = decodeUri ? DecodeUriComponent(trimmed) : UnescapeComponent(trimmed);
        if (decoded is "*" or "-")
        {
            return decoded;
        }

        if (decoded.Length == 0)
        {
            return "*";
        }

        var normalized = lower ? decoded.ToLowerInvariant() : decoded;
        return EscapeComponent(normalized);
    }

    private static (string Edition, string SwEdition, string TargetSw, string TargetHw, string Other) ExpandEdition(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return ("*", "*", "*", "*", "*");
        }

        var trimmed = value.Trim();
        if (trimmed is "*" or "-")
        {
            return (trimmed, "*", "*", "*", "*");
        }

        var decoded = DecodeUriComponent(trimmed);
        if (!decoded.StartsWith("~", StringComparison.Ordinal))
        {
            return (NormalizeDecodedField(decoded, lower: false), "*", "*", "*", "*");
        }

        var segments = decoded.Split('~');
        var swEdition = segments.Length > 1 ? NormalizeDecodedField(segments[1], lower: false) : "*";
        var targetSw = segments.Length > 2 ? NormalizeDecodedField(segments[2], lower: false) : "*";
        var targetHw = segments.Length > 3 ? NormalizeDecodedField(segments[3], lower: false) : "*";
        var other = segments.Length > 4 ? NormalizeDecodedField(segments[4], lower: false) : "*";

        return ("*", swEdition, targetSw, targetHw, other);
    }

    private static string NormalizeDecodedField(string? value, bool lower)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return "*";
        }

        var trimmed = value.Trim();
        if (trimmed is "*" or "-")
        {
            return trimmed;
        }

        var normalized = lower ? trimmed.ToLowerInvariant() : trimmed;
        if (normalized is "*" or "-")
        {
            return normalized;
        }

        return EscapeComponent(normalized);
    }

    private static string UnescapeComponent(string value)
    {
        var builder = new StringBuilder(value.Length);
        var escape = false;
        foreach (var ch in value)
        {
            if (escape)
            {
                builder.Append(ch);
                escape = false;
                continue;
            }

            if (ch == '\\')
            {
                escape = true;
                continue;
            }

            builder.Append(ch);
        }

        if (escape)
        {
            builder.Append('\\');
        }

        return builder.ToString();
    }

    private static string EscapeComponent(string value)
    {
        if (value.Length == 0)
        {
            return value;
        }

        var builder = new StringBuilder(value.Length * 2);
        foreach (var ch in value)
        {
            if (RequiresEscape(ch))
            {
                builder.Append('\\');
            }

            builder.Append(ch);
        }

        return builder.ToString();
    }

    private static bool RequiresEscape(char ch)
    {
        if (char.IsLetterOrDigit(ch))
        {
            return false;
        }

        if (char.IsWhiteSpace(ch))
        {
            return true;
        }

        return ch switch
        {
            '_' or '-' or '.' => false,
            // Keep wildcard markers literal only when entire component is wildcard handled earlier.
            '*' => true,
            _ => CharactersRequiringEscape.Contains(ch)
        };
    }

    private static string DecodeUriComponent(string value)
    {
        var builder = new StringBuilder(value.Length);
        for (var i = 0; i < value.Length; i++)
        {
            var ch = value[i];
            if (ch == '%' && i + 2 < value.Length && IsHex(value[i + 1]) && IsHex(value[i + 2]))
            {
                var hex = new string(new[] { value[i + 1], value[i + 2] });
                var decoded = (char)int.Parse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
                builder.Append(decoded);
                i += 2;
            }
            else
            {
                builder.Append(ch);
            }
        }

        return builder.ToString();
    }

    private static bool IsHex(char ch)
        => ch is >= '0' and <= '9' or >= 'A' and <= 'F' or >= 'a' and <= 'f';

    private static List<string> SplitComponents(string value)
    {
        var results = new List<string>();
        var builder = new StringBuilder();
        var escape = false;
        foreach (var ch in value)
        {
            if (escape)
            {
                builder.Append(ch);
                escape = false;
                continue;
            }

            if (ch == '\\')
            {
                builder.Append(ch);
                escape = true;
                continue;
            }

            if (ch == ':')
            {
                results.Add(builder.ToString());
                builder.Clear();
                continue;
            }

            builder.Append(ch);
        }

        results.Add(builder.ToString());
        return results;
    }
}
