"""
URL Validator for SSRF Prevention

Validates URLs to prevent Server-Side Request Forgery (SSRF) attacks
by blocking requests to internal/private networks and enforcing safe schemes.
"""

import ipaddress
import socket
from urllib.parse import urlparse
from typing import Optional
from loguru import logger

from .ip_ranges import PRIVATE_IP_RANGES as BLOCKED_IP_RANGES

# AWS metadata endpoint (commonly targeted in SSRF attacks)
# nosec B104 - Hardcoded IP is intentional for SSRF prevention (blocking AWS metadata endpoint)
AWS_METADATA_IP = "169.254.169.254"

# Allowed URL schemes
ALLOWED_SCHEMES = {"http", "https"}


def is_ip_blocked(
    ip_str: str, allow_localhost: bool = False, allow_private_ips: bool = False
) -> bool:
    """
    Check if an IP address is in a blocked range.

    Args:
        ip_str: IP address as string
        allow_localhost: Whether to allow localhost/loopback addresses
        allow_private_ips: Whether to allow all private/internal IPs plus localhost.
            This includes RFC1918 (10.x, 172.16-31.x, 192.168.x), CGNAT (100.64.x.x
            used by Podman/rootless containers), link-local (169.254.x.x), and IPv6
            private ranges (fc00::/7, fe80::/10). Use for trusted self-hosted services
            like SearXNG or Ollama in containerized environments.
            Note: AWS metadata endpoint (169.254.169.254) is ALWAYS blocked.

    Returns:
        True if IP is blocked, False otherwise
    """
    # Loopback ranges that can be allowed for trusted internal services
    # nosec B104 - These hardcoded IPs are intentional for SSRF allowlist
    LOOPBACK_RANGES = [
        ipaddress.ip_network("127.0.0.0/8"),  # IPv4 loopback
        ipaddress.ip_network("::1/128"),  # IPv6 loopback
    ]

    # Private/internal network ranges - allowed with allow_private_ips=True
    # nosec B104 - These hardcoded IPs are intentional for SSRF allowlist
    PRIVATE_RANGES = [
        # RFC1918 Private Ranges
        ipaddress.ip_network("10.0.0.0/8"),  # Class A private
        ipaddress.ip_network("172.16.0.0/12"),  # Class B private
        ipaddress.ip_network("192.168.0.0/16"),  # Class C private
        # Container/Virtual Network Ranges
        ipaddress.ip_network(
            "100.64.0.0/10"
        ),  # CGNAT - used by Podman/rootless containers
        ipaddress.ip_network(
            "169.254.0.0/16"
        ),  # Link-local (AWS metadata blocked separately)
        # IPv6 Private Ranges
        ipaddress.ip_network("fc00::/7"),  # IPv6 Unique Local Addresses
        ipaddress.ip_network("fe80::/10"),  # IPv6 Link-Local
    ]

    try:
        ip = ipaddress.ip_address(ip_str)

        # Unwrap IPv4-mapped IPv6 addresses (e.g. ::ffff:127.0.0.1 → 127.0.0.1)
        # These bypass IPv4 range checks if not converted.
        if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped:
            ip = ip.ipv4_mapped

        # ALWAYS block AWS metadata endpoint - critical SSRF target for credential theft
        if str(ip) == AWS_METADATA_IP:
            return True

        # Check if IP is in any blocked range
        for blocked_range in BLOCKED_IP_RANGES:
            if ip in blocked_range:
                # If allow_private_ips is True, skip blocking for private + loopback
                if allow_private_ips:
                    is_loopback = any(ip in lr for lr in LOOPBACK_RANGES)
                    is_private = any(ip in pr for pr in PRIVATE_RANGES)
                    if is_loopback or is_private:
                        continue
                # If allow_localhost is True, skip blocking for loopback only
                elif allow_localhost:
                    is_loopback = any(ip in lr for lr in LOOPBACK_RANGES)
                    if is_loopback:
                        continue
                return True

        return False

    except ValueError:
        # Invalid IP address
        return False


def validate_url(
    url: str,
    allow_localhost: bool = False,
    allow_private_ips: bool = False,
) -> bool:
    """
    Validate URL to prevent SSRF attacks.

    Checks:
    1. URL scheme is allowed (http/https only)
    2. Hostname is not an internal/private IP address
    3. Hostname does not resolve to an internal/private IP

    Args:
        url: URL to validate
        allow_localhost: Whether to allow localhost/loopback addresses.
            Set to True for trusted internal services like self-hosted
            search engines (e.g., searxng). Default False.
        allow_private_ips: Whether to allow all private/internal IPs plus localhost.
            This includes RFC1918 (10.x, 172.16-31.x, 192.168.x), CGNAT (100.64.x.x
            used by Podman/rootless containers), link-local (169.254.x.x), and IPv6
            private ranges (fc00::/7, fe80::/10). Use for trusted self-hosted services
            like SearXNG or Ollama in containerized environments.
            Note: AWS metadata endpoint (169.254.169.254) is ALWAYS blocked.

    Returns:
        True if URL is safe, False otherwise
    """
    try:
        parsed = urlparse(url)

        # Check scheme
        if parsed.scheme.lower() not in ALLOWED_SCHEMES:
            logger.warning(
                f"Blocked URL with invalid scheme: {parsed.scheme} - {url}"
            )
            return False

        hostname = parsed.hostname
        if not hostname:
            logger.warning(f"Blocked URL with no hostname: {url}")
            return False

        # Check if hostname is an IP address
        try:
            ip = ipaddress.ip_address(hostname)
            if is_ip_blocked(
                str(ip),
                allow_localhost=allow_localhost,
                allow_private_ips=allow_private_ips,
            ):
                logger.warning(
                    f"Blocked URL with internal/private IP: {hostname} - {url}"
                )
                return False
        except ValueError:
            # Not an IP address, it's a hostname - need to resolve it
            pass

        # Resolve hostname to IP and check
        try:
            # Get all IP addresses for hostname
            # nosec B104 - DNS resolution is intentional for SSRF prevention (checking if hostname resolves to private IP)
            addr_info = socket.getaddrinfo(
                hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM
            )

            for info in addr_info:
                ip_str = str(
                    info[4][0]
                )  # Extract IP address from addr_info tuple

                if is_ip_blocked(
                    ip_str,
                    allow_localhost=allow_localhost,
                    allow_private_ips=allow_private_ips,
                ):
                    logger.warning(
                        f"Blocked URL - hostname {hostname} resolves to "
                        f"internal/private IP: {ip_str} - {url}"
                    )
                    return False

        except socket.gaierror:
            logger.warning(f"Failed to resolve hostname {hostname}")
            return False
        except Exception:
            logger.exception("Error during hostname resolution")
            return False

        # URL passes all checks
        return True

    except Exception:
        logger.exception(f"Error validating URL {url}")
        return False


def get_safe_url(
    url: Optional[str], default: Optional[str] = None
) -> Optional[str]:
    """
    Get URL if it's safe, otherwise return default.

    Args:
        url: URL to validate
        default: Default value if URL is unsafe

    Returns:
        URL if safe, default otherwise
    """
    if not url:
        return default

    if validate_url(url):
        return url

    logger.warning(f"Unsafe URL rejected: {url}")
    return default
