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

namespace ClawdDotNet.Tools.WebFetch;

/// <summary>
/// Prüft Adressen, bevor das Tool sie abruft.
///
/// Hintergrund (S5): Die Domain-Whitelist wurde nur auf die ursprüngliche URL angewendet.
/// HttpClient folgt Weiterleitungen standardmäßig selbst — eine erlaubte Domain konnte
/// damit auf beliebige interne Adressen weiterleiten (Router, NAS, Git-Server im LAN,
/// Cloud-Metadatendienste unter 169.254.169.254). Der Agent hätte deren Inhalt
/// zurückgeliefert.
///
/// Deshalb: Weiterleitungen werden nicht mehr automatisch verfolgt, sondern einzeln
/// aufgelöst und JEDER Zwischenschritt erneut geprüft.
/// </summary>
public static class UrlGuard
{
    public static bool TryValidate(
        string? url,
        IReadOnlyCollection<string> allowedDomains,
        out Uri? uri,
        out string? error)
    {
        uri = null;
        error = null;

        if (string.IsNullOrWhiteSpace(url))
        {
            error = "Leere Adresse.";
            return false;
        }

        if (!Uri.TryCreate(url.Trim(), UriKind.Absolute, out var parsed))
        {
            error = "Keine gültige absolute URL.";
            return false;
        }

        if (parsed.Scheme != Uri.UriSchemeHttp && parsed.Scheme != Uri.UriSchemeHttps)
        {
            error = $"Schema '{parsed.Scheme}' ist nicht erlaubt — nur http und https.";
            return false;
        }

        if (IsPrivateOrLocal(parsed.Host))
        {
            error = $"Adresse '{parsed.Host}' liegt im lokalen oder privaten Netz und ist gesperrt.";
            return false;
        }

        if (!IsDomainAllowed(parsed.Host, allowedDomains))
        {
            error = $"Domain '{parsed.Host}' steht nicht auf der Whitelist dieses Agenten.";
            return false;
        }

        uri = parsed;
        return true;
    }

    /// <summary>
    /// Prüft den Host gegen die Whitelist. Erlaubt ist der Eintrag selbst und jede
    /// Subdomain davon.
    ///
    /// Die frühere Fassung entfernte "www." mit einem Ersetzen über die ganze
    /// Zeichenkette — aus "mywww.example.com" wurde dabei "myexample.com".
    /// </summary>
    public static bool IsDomainAllowed(string host, IReadOnlyCollection<string> allowedDomains)
    {
        if (allowedDomains.Count == 0)
            return false;

        var normalizedHost = NormalizeHost(host);

        foreach (var entry in allowedDomains)
        {
            if (string.IsNullOrWhiteSpace(entry))
                continue;

            var domain = NormalizeHost(entry);

            if (normalizedHost.Equals(domain, StringComparison.OrdinalIgnoreCase))
                return true;

            // Echte Subdomain — der Punkt verhindert, dass "example.com.attacker.net"
            // als Treffer für "example.com" durchgeht.
            if (normalizedHost.EndsWith("." + domain, StringComparison.OrdinalIgnoreCase))
                return true;
        }

        return false;
    }

    private static string NormalizeHost(string host)
    {
        var value = host.Trim().TrimEnd('.').ToLowerInvariant();
        return value.StartsWith("www.", StringComparison.Ordinal) ? value[4..] : value;
    }

    /// <summary>
    /// Erkennt Adressen im eigenen oder privaten Netz — einschließlich der
    /// Metadatendienste von Cloud-Anbietern.
    /// </summary>
    public static bool IsPrivateOrLocal(string host)
    {
        var value = host.Trim().Trim('[', ']').ToLowerInvariant();

        if (value is "localhost" || value.EndsWith(".localhost", StringComparison.Ordinal)
                                 || value.EndsWith(".local", StringComparison.Ordinal)
                                 || value.EndsWith(".internal", StringComparison.Ordinal))
        {
            return true;
        }

        if (!IPAddress.TryParse(value, out var ip))
            return false;

        if (IPAddress.IsLoopback(ip))
            return true;

        if (ip.AddressFamily == AddressFamily.InterNetwork)
        {
            var b = ip.GetAddressBytes();
            return b[0] switch
            {
                10 => true,                                   // 10.0.0.0/8
                127 => true,                                  // 127.0.0.0/8
                0 => true,                                    // 0.0.0.0/8
                169 when b[1] == 254 => true,                 // 169.254.0.0/16 (Metadaten)
                172 when b[1] >= 16 && b[1] <= 31 => true,    // 172.16.0.0/12
                192 when b[1] == 168 => true,                 // 192.168.0.0/16
                100 when b[1] >= 64 && b[1] <= 127 => true,   // 100.64.0.0/10 (CGNAT)
                _ => false
            };
        }

        if (ip.AddressFamily == AddressFamily.InterNetworkV6)
        {
            if (ip.IsIPv6LinkLocal || ip.IsIPv6SiteLocal)
                return true;

            // Unique Local Addresses fc00::/7
            var b = ip.GetAddressBytes();
            if ((b[0] & 0xFE) == 0xFC)
                return true;

            // IPv4-gemappte Adressen erneut als IPv4 prüfen
            if (ip.IsIPv4MappedToIPv6)
                return IsPrivateOrLocal(ip.MapToIPv4().ToString());
        }

        return false;
    }
}
