using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;

namespace StellaOps.Auth.Abstractions;

/// <summary>
/// Evaluates remote addresses against configured network masks.
/// </summary>
public sealed class NetworkMaskMatcher
{
    private readonly NetworkMask[] masks;
    private readonly bool matchAll;

    /// <summary>
    /// Creates a matcher from raw CIDR strings.
    /// </summary>
    /// <param name="values">Sequence of CIDR entries or IP addresses.</param>
    /// <exception cref="FormatException">Thrown when a value cannot be parsed.</exception>
    public NetworkMaskMatcher(IEnumerable<string>? values)
        : this(Parse(values))
    {
    }

    /// <summary>
    /// Creates a matcher from already parsed masks.
    /// </summary>
    /// <param name="masks">Sequence of network masks.</param>
    public NetworkMaskMatcher(IEnumerable<NetworkMask> masks)
    {
        ArgumentNullException.ThrowIfNull(masks);

        var unique = new HashSet<NetworkMask>();
        foreach (var mask in masks)
        {
            unique.Add(mask);
        }

        this.masks = unique.ToArray();
        matchAll = this.masks.Length == 1 && this.masks[0].PrefixLength == 0;
    }

    private NetworkMaskMatcher((bool MatchAll, NetworkMask[] Masks) parsed)
    {
        matchAll = parsed.MatchAll;
        masks = parsed.Masks;
    }

    /// <summary>
    /// Gets a matcher that allows every address.
    /// </summary>
    public static NetworkMaskMatcher AllowAll { get; } = new((true, Array.Empty<NetworkMask>()));

    /// <summary>
    /// Gets a matcher that denies every address (no masks configured).
    /// </summary>
    public static NetworkMaskMatcher DenyAll { get; } = new((false, Array.Empty<NetworkMask>()));

    /// <summary>
    /// Indicates whether this matcher has no masks configured and does not allow all.
    /// </summary>
    public bool IsEmpty => !matchAll && masks.Length == 0;

    /// <summary>
    /// Returns the configured masks.
    /// </summary>
    public IReadOnlyList<NetworkMask> Masks => masks;

    /// <summary>
    /// Checks whether the provided address matches any of the configured masks.
    /// </summary>
    /// <param name="address">Remote address to test.</param>
    /// <returns><c>true</c> when the address is allowed.</returns>
    public bool IsAllowed(IPAddress? address)
    {
        if (address is null)
        {
            return false;
        }

        if (matchAll)
        {
            return true;
        }

        if (masks.Length == 0)
        {
            return false;
        }

        foreach (var mask in masks)
        {
            if (mask.Contains(address))
            {
                return true;
            }
        }

        return false;
    }

    private static (bool MatchAll, NetworkMask[] Masks) Parse(IEnumerable<string>? values)
    {
        if (values is null)
        {
            return (false, Array.Empty<NetworkMask>());
        }

        var unique = new HashSet<NetworkMask>();

        foreach (var raw in values)
        {
            if (string.IsNullOrWhiteSpace(raw))
            {
                continue;
            }

            var trimmed = raw.Trim();

            if (IsAllowAll(trimmed))
            {
                return (true, Array.Empty<NetworkMask>());
            }

            if (!NetworkMask.TryParse(trimmed, out var mask))
            {
                throw new FormatException($"'{trimmed}' is not a valid network mask or IP address.");
            }

            unique.Add(mask);
        }

        return (false, unique.ToArray());
    }

    private static bool IsAllowAll(string value)
        => value is "*" or "0.0.0.0/0" or "::/0";
}
