using System.Collections.Immutable;
using System.Linq;
using System.Text;

namespace StellaOps.Feedser.Normalization.Identifiers;

/// <summary>
/// Represents a parsed Package URL (purl) identifier with canonical string rendering.
/// </summary>
public sealed class PackageUrl
{
    private PackageUrl(
        string type,
        ImmutableArray<string> namespaceSegments,
        string name,
        string? version,
        ImmutableArray<KeyValuePair<string, string>> qualifiers,
        ImmutableArray<string> subpathSegments,
        string original)
    {
        Type = type;
        NamespaceSegments = namespaceSegments;
        Name = name;
        Version = version;
        Qualifiers = qualifiers;
        SubpathSegments = subpathSegments;
        Original = original;
    }

    public string Type { get; }

    public ImmutableArray<string> NamespaceSegments { get; }

    public string Name { get; }

    public string? Version { get; }

    public ImmutableArray<KeyValuePair<string, string>> Qualifiers { get; }

    public ImmutableArray<string> SubpathSegments { get; }

    public string Original { get; }

    private static readonly HashSet<string> LowerCaseNamespaceTypes = new(StringComparer.OrdinalIgnoreCase)
    {
        "maven",
        "npm",
        "pypi",
        "nuget",
        "composer",
        "gem",
        "apk",
        "deb",
        "rpm",
        "oci",
    };

    private static readonly HashSet<string> LowerCaseNameTypes = new(StringComparer.OrdinalIgnoreCase)
    {
        "npm",
        "pypi",
        "nuget",
        "composer",
        "gem",
        "apk",
        "deb",
        "rpm",
        "oci",
    };

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

        var trimmed = value.Trim();
        if (!trimmed.StartsWith("pkg:", StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        var remainder = trimmed[4..];
        var firstSlash = remainder.IndexOf('/');
        if (firstSlash <= 0)
        {
            return false;
        }

        var type = remainder[..firstSlash].Trim().ToLowerInvariant();
        remainder = remainder[(firstSlash + 1)..];

        var subpathPart = string.Empty;
        var subpathIndex = remainder.IndexOf('#');
        if (subpathIndex >= 0)
        {
            subpathPart = remainder[(subpathIndex + 1)..];
            remainder = remainder[..subpathIndex];
        }

        var qualifierPart = string.Empty;
        var qualifierIndex = remainder.IndexOf('?');
        if (qualifierIndex >= 0)
        {
            qualifierPart = remainder[(qualifierIndex + 1)..];
            remainder = remainder[..qualifierIndex];
        }

        string? version = null;
        var versionIndex = remainder.LastIndexOf('@');
        if (versionIndex >= 0)
        {
            version = remainder[(versionIndex + 1)..];
            remainder = remainder[..versionIndex];
        }

        if (string.IsNullOrWhiteSpace(remainder))
        {
            return false;
        }

        var rawSegments = remainder.Split('/', StringSplitOptions.RemoveEmptyEntries);
        if (rawSegments.Length == 0)
        {
            return false;
        }

        var shouldLowerNamespace = LowerCaseNamespaceTypes.Contains(type);
        var shouldLowerName = LowerCaseNameTypes.Contains(type);

        var namespaceBuilder = ImmutableArray.CreateBuilder<string>(Math.Max(0, rawSegments.Length - 1));
        for (var i = 0; i < rawSegments.Length - 1; i++)
        {
            var segment = Uri.UnescapeDataString(rawSegments[i].Trim());
            if (segment.Length == 0)
            {
                continue;
            }

            if (shouldLowerNamespace)
            {
                segment = segment.ToLowerInvariant();
            }

            namespaceBuilder.Add(EscapePathSegment(segment));
        }

        var nameSegment = Uri.UnescapeDataString(rawSegments[^1].Trim());
        if (nameSegment.Length == 0)
        {
            return false;
        }

        if (shouldLowerName)
        {
            nameSegment = nameSegment.ToLowerInvariant();
        }

        var canonicalName = EscapePathSegment(nameSegment);
        var canonicalVersion = NormalizeComponent(version, escape: true, lowerCase: false);
        var qualifiers = ParseQualifiers(qualifierPart);
        var subpath = ParseSubpath(subpathPart);

        packageUrl = new PackageUrl(
            type,
            namespaceBuilder.ToImmutable(),
            canonicalName,
            canonicalVersion,
            qualifiers,
            subpath,
            trimmed);
        return true;
    }

    public static PackageUrl Parse(string value)
    {
        if (!TryParse(value, out var parsed))
        {
            throw new FormatException($"Input '{value}' is not a valid Package URL.");
        }

        return parsed!;
    }

    public string ToCanonicalString()
    {
        var builder = new StringBuilder("pkg:");
        builder.Append(Type);
        builder.Append('/');

        if (!NamespaceSegments.IsDefaultOrEmpty)
        {
            builder.Append(string.Join('/', NamespaceSegments));
            builder.Append('/');
        }

        builder.Append(Name);

        if (!string.IsNullOrEmpty(Version))
        {
            builder.Append('@');
            builder.Append(Version);
        }

        if (!Qualifiers.IsDefaultOrEmpty && Qualifiers.Length > 0)
        {
            builder.Append('?');
            builder.Append(string.Join('&', Qualifiers.Select(static kvp => $"{kvp.Key}={kvp.Value}")));
        }

        if (!SubpathSegments.IsDefaultOrEmpty && SubpathSegments.Length > 0)
        {
            builder.Append('#');
            builder.Append(string.Join('/', SubpathSegments));
        }

        return builder.ToString();
    }

    public override string ToString() => ToCanonicalString();

    private static ImmutableArray<KeyValuePair<string, string>> ParseQualifiers(string qualifierPart)
    {
        if (string.IsNullOrEmpty(qualifierPart))
        {
            return ImmutableArray<KeyValuePair<string, string>>.Empty;
        }

        var entries = qualifierPart.Split('&', StringSplitOptions.RemoveEmptyEntries);
        var map = new SortedDictionary<string, string>(StringComparer.Ordinal);
        foreach (var entry in entries)
        {
            var trimmed = entry.Trim();
            if (trimmed.Length == 0)
            {
                continue;
            }

            var equalsIndex = trimmed.IndexOf('=');
            if (equalsIndex <= 0)
            {
                continue;
            }

            var key = Uri.UnescapeDataString(trimmed[..equalsIndex]).Trim().ToLowerInvariant();
            var valuePart = equalsIndex < trimmed.Length - 1 ? trimmed[(equalsIndex + 1)..] : string.Empty;
            var value = NormalizeComponent(valuePart, escape: true, lowerCase: false);
            map[key] = value;
        }

        return map.Select(static kvp => new KeyValuePair<string, string>(kvp.Key, kvp.Value)).ToImmutableArray();
    }

    private static ImmutableArray<string> ParseSubpath(string subpathPart)
    {
        if (string.IsNullOrEmpty(subpathPart))
        {
            return ImmutableArray<string>.Empty;
        }

        var segments = subpathPart.Split('/', StringSplitOptions.RemoveEmptyEntries);
        var builder = ImmutableArray.CreateBuilder<string>(segments.Length);
        foreach (var raw in segments)
        {
            var segment = Uri.UnescapeDataString(raw.Trim());
            if (segment.Length == 0)
            {
                continue;
            }

            builder.Add(EscapePathSegment(segment));
        }

        return builder.ToImmutable();
    }

    private static string NormalizeComponent(string? value, bool escape, bool lowerCase)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }

        var unescaped = Uri.UnescapeDataString(value.Trim());
        if (lowerCase)
        {
            unescaped = unescaped.ToLowerInvariant();
        }

        return escape ? Uri.EscapeDataString(unescaped) : unescaped;
    }

    private static string EscapePathSegment(string value)
    {
        return Uri.EscapeDataString(value);
    }
}
