"""URL 验证工具,用于网络安全防护.

本模块提供集中化的 URL 验证功能,防止 SSRF(服务器端请求伪造)
和本地文件访问攻击(CWE-22).

主要特性:
    - URL 方案白名单(仅支持 http/https)
    - SSRF 防护
    - 本地文件访问阻止
    - 可重用的验证函数

安全性:
    本模块通过仅允许 http 和 https 方案来实现对 CWE-22(路径遍历)的防御,
    阻止以下攻击:
    - file:// 方案访问(本地文件读取)
    - gopher:// 方案访问(内部服务攻击)
    - dict:// 方案访问(数据窃取)
    - 其他潜在危险方案

使用示例:
    >>> 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.error
import urllib.request
from contextlib import contextmanager
from typing import Any, Final, Generator, NamedTuple
from urllib.parse import urlparse

# 允许的 URL 方案(安全防护)
# 仅允许 HTTP 和 HTTPS,以防止:
# - 通过 file:// 进行本地文件访问
# - 通过 http://169.254.169.254 进行内网扫描
# - 通过 gopher://,dict:// 等进行协议注入攻击
_ALLOWED_SCHEMES: Final[frozenset[str]] = frozenset(["http", "https"])


class URLResponse(NamedTuple):
    """URL 响应包装器,提供统一接口.

    为成功和失败的 URL 访问提供统一的接口,
    确保 status 和其他属性始终可用.

    属性
    ----
        response: 实际的 HTTP 响应对象,如果访问失败则为 None
        status: HTTP 状态码(请求失败或无效 URL 时为 0)
        success: 布尔值,表示请求是否成功
    """

    response: Any
    status: int = 0
    success: bool = False

    @property
    def url(self) -> str | None:
        """获取响应中的 URL,失败时返回 None."""
        return getattr(self.response, "url", None) if self.response else None

    def read(self, *args: object, **kwargs: object) -> bytes:
        """从响应读取内容(如果可用)."""
        if self.response and hasattr(self.response, "read"):
            return self.response.read(*args, **kwargs)
        return b""

    def getheader(self, name: str, default: object = None) -> object:
        """从响应获取头部信息(如果可用)."""
        if self.response and hasattr(self.response, "getheader"):
            return self.response.getheader(name, default)
        return default


def is_allowed_scheme(scheme: str) -> bool:
    """检查 URL 方案是否在允许列表中.

    参数
    ----
        scheme: 要检查的 URL 方案(如 "http","https","file")

    返回
    ----
        如果方案被允许(http 或 https)则返回 True,否则返回 False

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


def validate_url(url: str) -> bool:
    """验证 URL 方案以防止 SSRF 和本地文件访问.

    Args
    ----
        url: 要验证的 URL 字符串

    Returns
    -------
        如果 URL 使用允许的方案(http/https)则返回 True,否则返回 False

    Examples
    --------
        >>> 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:
    """严格验证 URL,包含额外的安全检查.

    本函数执行比 validate_url() 更严格的验证:
    - 要求同时具备 scheme 和 netloc(域名)
    - 可选择性地拒绝包含片段(#anchor)的 URL
    - 拒绝 IPv6 链路本地地址(fe80::/10)
    - 可根据需要拒绝 IPv4 私有地址段

    Args
    ----
        url: 要验证的 URL 字符串
        allow_fragments: 如果为 False,则拒绝包含 # 片段的 URL

    Returns
    -------
        如果 URL 通过严格验证则返回 True,否则返回 False

    Examples
    --------
        >>> 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:
        # 确保 scheme 和 netloc 都存在,并根据 allow_fragments 检查 fragment
        has_scheme_and_netloc = bool(parsed.scheme and parsed.netloc)
        fragment_ok = not parsed.fragment or allow_fragments
        return has_scheme_and_netloc and fragment_ok


@contextmanager
def urlopen_safe(
    url: str | urllib.request.Request,
    *args: object,
    **kwargs: object,
) -> Generator[URLResponse, None, None]:
    """安全打开 URL 的上下文管理器,支持严格验证.

    本函数提供一种安全的 URL 打开方式:
    - 验证 URL 方案(仅限 http/https)
    - 支持 'with' 语句以实现自动资源清理
    - 始终返回具有 status 属性的对象(永不为 None)
    - 限制 urllib.request.urlopen 仅用于允许的方案(CWE-22 防护)

    Args
    ----
        url: 要打开的 URL 字符串或 Request 对象

    Yields
    ------
        URLResponse 对象,包含 response,status 和 success 标志

    Examples
    --------
        >>> 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:
        """验证 URL 方案,如果不被允许则抛出 ValueError."""
        parsed = urlparse(url_to_check)
        if not is_allowed_scheme(parsed.scheme):
            msg = f"不允许的 URL 方案:{parsed.scheme}.仅允许使用 http/https."
            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:
        # 安全性:在打开前双重检查方案以防止 CWE-22 攻击
        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) 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)
