using System.Net;
using System.Net.Sockets;

namespace Sunfish.Net;

/// <summary>
/// Listas de direcciones como AllowFrom: IP sueltas ("127.0.0.1", "::1"), redes en notacion CIDR
/// ("192.168.1.0/24", "fd00::/8") o "*" (cualquiera). Las IPv4 que llegan como IPv6
/// (::ffff:a.b.c.d) se comparan como IPv4.
/// </summary>
public static class AddressRules
{
    public static bool IsValid(string rule) => rule == "*" || TryParse(rule, out _, out _);

    public static bool Matches(IEnumerable<string> rules, IPAddress? address)
    {
        if (address is null)
            return false;
        if (address.IsIPv4MappedToIPv6)
            address = address.MapToIPv4();
        foreach (var rule in rules)
        {
            if (rule == "*")
                return true;
            if (TryParse(rule, out var network, out int prefix) && InNetwork(address, network, prefix))
                return true;
        }
        return false;
    }

    private static bool TryParse(string rule, out IPAddress network, out int prefix)
    {
        network = IPAddress.None;
        prefix = 0;
        string text = rule.Trim();
        int slash = text.IndexOf('/');
        if (!IPAddress.TryParse(slash < 0 ? text : text[..slash], out var parsed))
            return false;
        network = parsed.IsIPv4MappedToIPv6 ? parsed.MapToIPv4() : parsed;
        int max = network.AddressFamily == AddressFamily.InterNetwork ? 32 : 128;
        if (slash < 0)
        {
            prefix = max;
            return true;
        }
        return int.TryParse(text[(slash + 1)..], out prefix) && prefix >= 0 && prefix <= max;
    }

    private static bool InNetwork(IPAddress address, IPAddress network, int prefix)
    {
        if (address.AddressFamily != network.AddressFamily)
            return false;
        var a = address.GetAddressBytes();
        var n = network.GetAddressBytes();
        int full = prefix / 8, rest = prefix % 8;
        for (int i = 0; i < full; i++)
            if (a[i] != n[i])
                return false;
        if (rest == 0)
            return true;
        int mask = 0xFF << (8 - rest) & 0xFF;
        return (a[full] & mask) == (n[full] & mask);
    }
}
