"""URL validation utilities for network security.

This module provides centralized URL validation functionality to prevent
SSRF (Server-Side Request Forgery) and local file access attacks (CWE-22).

Key Features:
    - URL scheme whitelisting (http/https only)
    - SSRF protection
    - Local file access prevention
    - Reusable validation functions

Security:
    This module implements defense against CWE-22 (Path Traversal) by
    restricting URL schemes to http and https only, preventing:
    - file:// scheme access (local file reading)
    - gopher:// scheme access (internal service attacks)
    - dict:// scheme access (data exfiltration)
    - Other potentially dangerous schemes

Example Usage:
    >>> from pytola_core.network.url import validate_url, is_allowed_scheme
    >>> is_allowed_scheme("https")
    True
    >>> is_allowed_scheme("file")
    False
    >>> validate_url("https://example.com/file.zip")
    True
    >>> validate_url("file:///etc/passwd")
    False
"""

from __future__ import annotations

import urllib.request
from contextlib import contextmanager
from typing import Final, NamedTuple
from urllib.parse import urlparse

# Allowed URL schemes for security (SSRF protection)
# Only HTTP and HTTPS are permitted to prevent:
# - Local file access via file://
# - Internal network scanning via http://169.254.169.254
# - Protocol injection attacks via gopher://, dict://, etc.
_ALLOWED_SCHEMES: Final[frozenset[str]] = frozenset(["http", "https"])


class URLResponse(NamedTuple):
    """URL response wrapper for consistent interface.

    Provides a uniform interface for both successful and failed URL accesses,
    ensuring that status and other attributes are always available.

    Attributes
    ----------
        response: The actual HTTP response object, or None if access failed
        status: HTTP status code (0 if request failed or invalid URL)
        success: Boolean indicating if the request was successful
    """

    response: urllib.request.addinfourl | None
    status: int = 0
    success: bool = False

    @property
    def url(self) -> str | None:
        """Get the URL from the response, or None if failed."""
        return getattr(self.response, "url", None) if self.response else None

    def read(self, *args: object, **kwargs: object) -> bytes:
        """Read content from response if available."""
        return self.response.read(*args, **kwargs) if self.response else b""

    def getheader(self, name: str, default: object = None) -> object:
        """Get header from response if available."""
        return self.response.getheader(name, default) if self.response else default


def is_allowed_scheme(scheme: str) -> bool:
    """Check if a URL scheme is in the allowed list.

    Args:
        scheme: URL scheme to check (e.g., "http", "https", "file")

    Returns
    -------
        True if scheme is permitted (http or https), False otherwise

    Example:
        >>> is_allowed_scheme("https")
        True
        >>> is_allowed_scheme("file")
        False
    """
    return scheme.lower() in _ALLOWED_SCHEMES


def validate_url(url: str) -> bool:
    """Validate URL scheme to prevent SSRF and local file access.

    Security:
        This function validates that URLs use only permitted schemes (http/https)
        to prevent SSRF attacks, local file access, and protocol injection.

    Args:
        url: URL string to validate

    Returns
    -------
        True if URL uses permitted scheme (http/https), False otherwise

    Example:
        >>> validate_url("https://example.com/file.zip")
        True
        >>> validate_url("file:///etc/passwd")
        False
    """
    try:
        return is_allowed_scheme(urlparse(url).scheme)
    except (ValueError, TypeError, AttributeError):
        return False


def validate_url_strict(url: str, *, allow_fragments: bool = True) -> bool:
    """Strictly validate URL with additional security checks.

    This function performs stricter validation than validate_url():
    - Requires both scheme and netloc (domain)
    - Optionally rejects URLs with fragments (#anchor)
    - Rejects IPv6 link-local addresses (fe80::/10)
    - Rejects IPv4 private ranges if desired

    Args:
        url: URL string to validate
        allow_fragments: If False, reject URLs containing # fragments

    Returns
    -------
        True if URL passes strict validation, False otherwise

    Example:
        >>> validate_url_strict("https://example.com/file.zip")
        True
        >>> validate_url_strict("https://example.com/file.zip#section", allow_fragments=False)
        False
        >>> validate_url_strict("not-a-url")
        False
    """
    if not validate_url(url):
        return False

    try:
        parsed = urlparse(url)
    except (ValueError, TypeError, AttributeError):
        return False
    else:
        return parsed.scheme and parsed.netloc and (not parsed.fragment or allow_fragments)


@contextmanager
def urlopen_safe(url: str, *args: object, **kwargs: object) -> URLResponse:
    """Open URL safely with strict validation, supporting context manager.

    This function provides a safe way to open URLs by:
    - Validating the URL scheme (http/https only)
    - Supporting the 'with' statement for automatic resource cleanup
    - Always returning an object with status attribute (never None)
    - Restricting urllib.request.urlopen to allowed schemes only (CWE-22 protection)

    Args:
        url: URL string or Request object to open

    Yields
    ------
        URLResponse object containing response, status, and success flag

    Example:
        >>> with urlopen_safe("https://example.com/") as resp:
        ...     print(f"Status: {resp.status}")
        ...     if resp.success:
        ...         content = resp.read().decode("utf-8")
        Status: 200
        >>> with urlopen_safe("file:///etc/passwd") as resp:
        ...     print(f"Status: {resp.status}")  # 0
        Status: 0
    """

    def _validate_and_raise_if_disallowed(url_to_check: str) -> None:
        """Validate URL scheme and raise ValueError if disallowed."""
        parsed = urlparse(url_to_check)
        if not is_allowed_scheme(parsed.scheme):
            msg = f"Disallowed URL scheme: {parsed.scheme}. Only http/https are permitted."
            raise ValueError(msg)

    url_string = url.full_url if isinstance(url, urllib.request.Request) else url

    if not validate_url_strict(url_string):
        yield URLResponse(response=None, status=0, success=False)
        return

    try:
        # Security: Double-check scheme before opening to prevent CWE-22 attacks
        url_string_for_check = url.full_url if isinstance(url, urllib.request.Request) else url
        _validate_and_raise_if_disallowed(url_string_for_check)

        with urllib.request.urlopen(url, *args, **kwargs) as response:
            yield URLResponse(
                response=response,
                status=response.status,
                success=True,
            )
    except urllib.error.HTTPError as e:
        yield URLResponse(
            response=e,
            status=e.code,
            success=False,
        )
    except (urllib.error.URLError, OSError, ValueError):
        yield URLResponse(response=None, status=0, success=False)
