# -*- coding: utf-8 -*-
"""HTTP 抓取器 - 复用自 pm001-crawler"""

from __future__ import annotations

import time
from dataclasses import dataclass
from typing import Any
from urllib.parse import urljoin

import requests


DEFAULT_USER_AGENT = (
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
    "AppleWebKit/537.36 (KHTML, like Gecko) "
    "Chrome/126.0.0.0 Safari/537.36"
)


@dataclass(slots=True)
class FetcherConfig:
    base_url: str
    timeout: float = 10.0
    delay_ms: int = 500
    retries: int = 3
    user_agent: str = DEFAULT_USER_AGENT


class ForumFetcher:
    def __init__(self, config: FetcherConfig) -> None:
        self.config = config
        self.session = requests.Session()
        self.session.headers.update(
            {
                "User-Agent": config.user_agent,
                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
                "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
                "Connection": "keep-alive",
                "Referer": config.base_url,
            }
        )

    def absolute_url(self, path_or_url: str) -> str:
        return urljoin(self.config.base_url, path_or_url)

    def get(self, path_or_url: str, params: dict[str, Any] | None = None) -> tuple[str, str]:
        url = self.absolute_url(path_or_url)
        last_error: Exception | None = None

        for attempt in range(1, self.config.retries + 1):
            try:
                response = self.session.get(
                    url,
                    params=params,
                    timeout=self.config.timeout,
                )
                response.raise_for_status()
                text = self._decode_response(response)
                self._sleep()
                return text, response.url
            except requests.RequestException as exc:
                last_error = exc
                if attempt >= self.config.retries:
                    break
                time.sleep(min(2**attempt, 5))

        raise RuntimeError(f"请求失败: {url}") from last_error

    def _decode_response(self, response: requests.Response) -> str:
        content = response.content
        ascii_head = content[:2048].decode("ascii", errors="ignore").lower()

        encodings: list[str | None] = []
        if "charset=gb" in ascii_head:
            encodings.extend(["gb18030", "gbk"])
        elif "charset=utf-8" in ascii_head:
            encodings.append("utf-8")

        apparent = response.apparent_encoding
        declared = response.encoding
        if declared and declared.lower() not in {"iso-8859-1", "ascii"}:
            encodings.append(declared)
        encodings.extend([apparent, "gb18030", "gbk", "utf-8", declared])

        seen: set[str] = set()
        for encoding in encodings:
            if not encoding:
                continue
            normalized = encoding.lower()
            if normalized in seen:
                continue
            seen.add(normalized)
            try:
                return content.decode(encoding, errors="ignore")
            except (LookupError, UnicodeDecodeError):
                continue
        return response.text

    def _sleep(self) -> None:
        if self.config.delay_ms > 0:
            time.sleep(self.config.delay_ms / 1000.0)
