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

namespace StellaOps.Auth.Abstractions;

/// <summary>
/// Represents an IP network expressed in CIDR notation.
/// </summary>
public readonly record struct NetworkMask
{
    private readonly IPAddress address;

    /// <summary>
    /// Initialises a new <see cref="NetworkMask"/>.
    /// </summary>
    /// <param name="network">Canonical network address with host bits zeroed.</param>
    /// <param name="prefixLength">Prefix length (0-32 for IPv4, 0-128 for IPv6).</param>
    public NetworkMask(IPAddress network, int prefixLength)
    {
        ArgumentNullException.ThrowIfNull(network);

        var maxPrefix = GetMaxPrefix(network);
        if (prefixLength is < 0 or > 128 || prefixLength > maxPrefix)
        {
            throw new ArgumentOutOfRangeException(nameof(prefixLength), $"Prefix length must be between 0 and {maxPrefix} for {network.AddressFamily}.");
        }

        address = Normalize(network, prefixLength);
        PrefixLength = prefixLength;
    }

    /// <summary>
    /// Canonical network address with host bits zeroed.
    /// </summary>
    public IPAddress Network => address;

    /// <summary>
    /// Prefix length.
    /// </summary>
    public int PrefixLength { get; }

    /// <summary>
    /// Attempts to parse the supplied value as CIDR notation or a single IP address.
    /// </summary>
    /// <exception cref="FormatException">Thrown when the input is not recognised.</exception>
    public static NetworkMask Parse(string value)
    {
        if (!TryParse(value, out var mask))
        {
            throw new FormatException($"'{value}' is not a valid CIDR or IP address.");
        }

        return mask;
    }

    /// <summary>
    /// Attempts to parse the supplied value as CIDR notation or a single IP address.
    /// </summary>
    public static bool TryParse(string? value, out NetworkMask mask)
    {
        mask = default;

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

        var trimmed = value.Trim();
        var slashIndex = trimmed.IndexOf('/', StringComparison.Ordinal);

        if (slashIndex < 0)
        {
            if (!IPAddress.TryParse(trimmed, out var singleAddress))
            {
                return false;
            }

            var defaultPrefix = singleAddress.AddressFamily == AddressFamily.InterNetwork ? 32 : 128;
            mask = new NetworkMask(singleAddress, defaultPrefix);
            return true;
        }

        var addressPart = trimmed[..slashIndex];
        var prefixPart = trimmed[(slashIndex + 1)..];

        if (!IPAddress.TryParse(addressPart, out var networkAddress))
        {
            return false;
        }

        if (!int.TryParse(prefixPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out var prefixLength))
        {
            return false;
        }

        try
        {
            mask = new NetworkMask(networkAddress, prefixLength);
            return true;
        }
        catch (ArgumentOutOfRangeException)
        {
            return false;
        }
    }

    /// <summary>
    /// Determines whether the provided address belongs to this network.
    /// </summary>
    public bool Contains(IPAddress address)
    {
        ArgumentNullException.ThrowIfNull(address);

        if (address.AddressFamily != this.address.AddressFamily)
        {
            return false;
        }

        if (PrefixLength == 0)
        {
            return true;
        }

        var targetBytes = address.GetAddressBytes();
        var networkBytes = this.address.GetAddressBytes();

        var fullBytes = PrefixLength / 8;
        for (var i = 0; i < fullBytes; i++)
        {
            if (targetBytes[i] != networkBytes[i])
            {
                return false;
            }
        }

        var remainder = PrefixLength % 8;
        if (remainder == 0)
        {
            return true;
        }

        var mask = (byte)(0xFF << (8 - remainder));
        return (targetBytes[fullBytes] & mask) == networkBytes[fullBytes];
    }

    private static int GetMaxPrefix(IPAddress address)
        => address.AddressFamily == AddressFamily.InterNetwork ? 32 :
           address.AddressFamily == AddressFamily.InterNetworkV6 ? 128 :
           throw new ArgumentOutOfRangeException(nameof(address), $"Unsupported address family {address.AddressFamily}.");

    private static IPAddress Normalize(IPAddress address, int prefixLength)
    {
        var bytes = address.GetAddressBytes();

        var fullBytes = prefixLength / 8;
        var remainder = prefixLength % 8;

        if (fullBytes < bytes.Length)
        {
            if (remainder > 0)
            {
                var mask = (byte)(0xFF << (8 - remainder));
                bytes[fullBytes] &= mask;
                fullBytes++;
            }

            for (var index = fullBytes; index < bytes.Length; index++)
            {
                bytes[index] = 0;
            }
        }

        return new IPAddress(bytes);
    }

    /// <inheritdoc />
    public override string ToString()
        => $"{Network}/{PrefixLength}";
}
