"""Security utilities for URL validation."""

import ipaddress
import socket
from urllib.parse import urlparse

_blocked_hostnames = {
    "localhost",
    "metadata.google.internal",
    "metadata.internal",
    "metadata.azure.com",
}


def is_safe_url(url: str) -> bool:
    """Check if URL is safe (not pointing to private/internal networks).

    Validates against:
    - Private/loopback/reserved/link-local/unspecified IPs
    - IPv6-mapped IPv4 addresses (e.g. ::ffff:127.0.0.1)
    - Common metadata/internal hostnames
    - Non-HTTP schemes
    """
    try:
        parsed = urlparse(url)
        if parsed.scheme not in ("http", "https"):
            return False
        hostname = parsed.hostname
        if not hostname:
            return False
        hostname_lower = hostname.lower()
        if hostname_lower in _blocked_hostnames:
            return False
        if hostname_lower.endswith((".internal", ".local", ".localhost")):
            return False
        try:
            ip = ipaddress.ip_address(hostname)
            if _is_unsafe_ip(ip):
                return False
        except ValueError:
            # hostname is a domain — resolve DNS and check IPs
            try:
                addr_infos = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
                for family, type_, proto, canonname, sockaddr in addr_infos:
                    ip = ipaddress.ip_address(sockaddr[0])
                    if _is_unsafe_ip(ip):
                        return False
            except (socket.gaierror, socket.timeout, OSError):
                return False  # fail-closed on DNS errors
        return True
    except Exception:
        return False


def _is_unsafe_ip(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
    """Check if an IP address is unsafe (private, loopback, etc.).

    Handles IPv6-mapped IPv4 addresses by checking the embedded IPv4.
    """
    if ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_link_local or ip.is_unspecified:
        return True
    # Check IPv6-mapped IPv4 (e.g. ::ffff:127.0.0.1)
    if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped is not None:
        mapped = ip.ipv4_mapped
        if mapped.is_private or mapped.is_loopback or mapped.is_reserved or mapped.is_link_local:
            return True
    return False


async def is_safe_url_async(url: str) -> bool:
    """Async variant of is_safe_url with DNS resolution."""
    try:
        parsed = urlparse(url)
        if parsed.scheme not in ("http", "https"):
            return False
        hostname = parsed.hostname
        if not hostname:
            return False
        hostname_lower = hostname.lower()
        if hostname_lower in _blocked_hostnames:
            return False
        if hostname_lower.endswith((".internal", ".local", ".localhost")):
            return False
        try:
            ip = ipaddress.ip_address(hostname)
            if _is_unsafe_ip(ip):
                return False
        except ValueError:
            try:
                import asyncio
                addr_infos = await asyncio.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
                for family, type_, proto, canonname, sockaddr in addr_infos:
                    ip = ipaddress.ip_address(sockaddr[0])
                    if _is_unsafe_ip(ip):
                        return False
            except (socket.gaierror, socket.timeout, OSError):
                return False
        return True
    except Exception:
        return False
