"""Remote fetch layer for candidate images, pages, and stylesheets.

Extracted from sqlite_store.py. This is the pure network boundary (HTTP fetch
+ SSRF guard); HTML/CSS parsing of the fetched content stays in the store. Tests
monkeypatch _open_url / _assert_public_http_url / _fetch_url_bytes here.
"""

from __future__ import annotations

import ipaddress
import socket
from urllib.parse import urlparse
from urllib.request import HTTPRedirectHandler, Request, build_opener

from rights_filter.server.store_url_utils import _is_http_url

CANDIDATE_IMAGE_FETCH_LIMIT = 8_000_000
CANDIDATE_IMAGE_FETCH_HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/124.0 Safari/537.36 CopyrighterCandidateCollector/0.1"
    ),
    "Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
    "Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
    "Sec-Fetch-Dest": "image",
    "Sec-Fetch-Mode": "no-cors",
    "Sec-Fetch-Site": "cross-site",
}
PAGE_FETCH_HEADERS = {
    "User-Agent": CANDIDATE_IMAGE_FETCH_HEADERS["User-Agent"],
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/*,*/*;q=0.8",
    "Accept-Language": CANDIDATE_IMAGE_FETCH_HEADERS["Accept-Language"],
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "cross-site",
}
STYLESHEET_FETCH_HEADERS = {
    "User-Agent": CANDIDATE_IMAGE_FETCH_HEADERS["User-Agent"],
    "Accept": "text/css,*/*;q=0.8",
    "Accept-Language": CANDIDATE_IMAGE_FETCH_HEADERS["Accept-Language"],
    "Sec-Fetch-Dest": "style",
    "Sec-Fetch-Mode": "no-cors",
    "Sec-Fetch-Site": "cross-site",
}


def _fetch_url_bytes(
    url: str,
    timeout: int = 10,
    limit: int = CANDIDATE_IMAGE_FETCH_LIMIT,
    referer_url: str = "",
) -> bytes:
    headers = _headers_with_referer(CANDIDATE_IMAGE_FETCH_HEADERS, referer_url)
    return _fetch_url_bytes_with_headers(url, headers, timeout, limit)


def _fetch_page_url_bytes(url: str, timeout: int = 10, limit: int = CANDIDATE_IMAGE_FETCH_LIMIT) -> bytes:
    return _fetch_url_bytes_with_headers(url, PAGE_FETCH_HEADERS, timeout, limit)


def _fetch_stylesheet_url_bytes(url: str, timeout: int = 10, limit: int = CANDIDATE_IMAGE_FETCH_LIMIT) -> bytes:
    return _fetch_url_bytes_with_headers(url, STYLESHEET_FETCH_HEADERS, timeout, limit)


def _assert_public_http_url(url: str) -> None:
    # SSRF guard: candidate image / page / stylesheet URLs come from external
    # search-result content (attacker-influenceable), so refuse to fetch
    # anything that resolves to a non-public address (loopback, RFC1918,
    # link-local cloud-metadata 169.254.169.254, etc.).
    parsed = urlparse(url)
    if parsed.scheme not in {"http", "https"} or not parsed.hostname:
        raise ValueError("only public http(s) URLs may be fetched")
    port = parsed.port or (443 if parsed.scheme == "https" else 80)
    try:
        infos = socket.getaddrinfo(parsed.hostname, port, proto=socket.IPPROTO_TCP)
    except OSError as exc:
        raise ValueError("could not resolve fetch host") from exc
    for *_, sockaddr in infos:
        ip = ipaddress.ip_address(sockaddr[0])
        if (
            ip.is_private
            or ip.is_loopback
            or ip.is_link_local
            or ip.is_reserved
            or ip.is_multicast
            or ip.is_unspecified
        ):
            raise ValueError("refusing to fetch internal address")


class _ValidatingRedirectHandler(HTTPRedirectHandler):
    # Re-run the SSRF guard on every redirect hop so a public URL cannot bounce
    # to an internal address.
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        _assert_public_http_url(newurl)
        return super().redirect_request(req, fp, code, msg, headers, newurl)


_SSRF_SAFE_OPENER = build_opener(_ValidatingRedirectHandler())


def _open_url(request: Request, timeout: int):
    return _SSRF_SAFE_OPENER.open(request, timeout=timeout)


def _fetch_url_bytes_with_headers(
    url: str,
    headers: dict[str, str],
    timeout: int,
    limit: int,
) -> bytes:
    _assert_public_http_url(url)
    request = Request(url, headers=headers)
    with _open_url(request, timeout) as response:
        data = response.read(limit + 1)
    if len(data) > limit:
        raise ValueError("candidate image exceeds size limit")
    return data


def _headers_with_referer(headers: dict[str, str], referer_url: str) -> dict[str, str]:
    if not referer_url or not _is_http_url(referer_url):
        return headers
    return {**headers, "Referer": referer_url}
