#!/usr/bin/env python3
"""
Shared HTTP utilities for Athena adapters.

Purpose: Make adapter traffic look like real browser requests
instead of bot signatures. Centralized so all adapters benefit.

Features:
- Realistic User-Agent rotation (Chrome/Firefox/Safari on Linux/Windows/macOS)
- Standard browser headers (Accept, Accept-Language, DNT)
- Jittered sleep to break mechanical timing patterns
- Exponential backoff on 429/503 responses
"""

import random
import time
import urllib.request
import urllib.error


# Realistic User-Agent strings — rotated per session
USER_AGENTS = [
    # Chrome on Linux
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
    # Chrome on Windows
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
    # Firefox on Linux
    "Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0",
    # Firefox on Windows
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0",
    # Safari on macOS
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15",
]

# Standard browser headers that every real request includes
BROWSER_HEADERS = {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate, br",
    "DNT": "1",
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Sec-Fetch-User": "?1",
    "Upgrade-Insecure-Requests": "1",
}


class HTTPClient:
    """Browser-like HTTP client for adapters.

    Usage:
        client = HTTPClient(name="arxiv")
        data = client.get("https://example.com/api")
        client.jitter_sleep(3)  # polite spacing with randomness
    """

    def __init__(self, name: str = "athena", api_mode: bool = False):
        """
        Args:
            name: Adapter name for User-Agent identification fallback.
            api_mode: If True, use Accept: application/json headers
                     (for JSON APIs). If False, use browser-like HTML headers.
        """
        self.name = name
        self.api_mode = api_mode
        # Pick a User-Agent once per session — avoids fingerprint rotation
        # which is MORE suspicious than sticking to one identity.
        self.user_agent = random.choice(USER_AGENTS)

    def _headers(self) -> dict:
        """Build headers dict for a request."""
        headers = {"User-Agent": self.user_agent}

        if self.api_mode:
            headers.update({
                "Accept": "application/json, text/json, */*;q=0.8",
                "Accept-Language": "en-US,en;q=0.9",
                "Accept-Encoding": "gzip, deflate, br",
                "DNT": "1",
            })
        else:
            headers.update(BROWSER_HEADERS)

        return headers

    def get(self, url: str, extra_headers: dict | None = None,
            timeout: int = 30, max_retries: int = 2,
            backoff_base: float = 2.0) -> bytes | None:
        """Make a GET request with browser-like headers and retry/backoff.

        Args:
            url: Request URL.
            extra_headers: Additional headers to merge in.
            timeout: Request timeout in seconds.
            max_retries: Max retry attempts on 429/503.
            backoff_base: Base seconds for exponential backoff (2^n * base).

        Returns:
            Response body bytes, or None on persistent failure.
        """
        headers = self._headers()
        if extra_headers:
            headers.update(extra_headers)

        req = urllib.request.Request(url, headers=headers)

        for attempt in range(max_retries + 1):
            try:
                with urllib.request.urlopen(req, timeout=timeout) as resp:
                    return resp.read()
            except urllib.error.HTTPError as e:
                if e.code in (429, 503):
                    wait = backoff_base ** (attempt + 1) + random.uniform(0, 1)
                    if attempt < max_retries:
                        print(f"    HTTP {e.code}, retrying in {wait:.1f}s")
                        time.sleep(wait)
                        continue
                    print(f"    HTTP {e.code} after {max_retries} retries, giving up")
                    return None
                # Other HTTP errors — don't retry
                print(f"    HTTP {e.code} for {url[:80]}")
                return None
            except urllib.error.URLError as e:
                if attempt < max_retries:
                    wait = backoff_base ** (attempt + 1)
                    print(f"    URLError: {e.reason}, retrying in {wait:.1f}s")
                    time.sleep(wait)
                    continue
                print(f"    URLError after retries: {e.reason}")
                return None
            except Exception as e:
                print(f"    Request error: {e}")
                return None

        return None

    def get_json(self, url: str, extra_headers: dict | None = None,
                 timeout: int = 30, max_retries: int = 2) -> dict | list | None:
        """Make a GET request and parse JSON response."""
        import json
        data = self.get(url, extra_headers=extra_headers, timeout=timeout,
                       max_retries=max_retries)
        if data is None:
            return None
        try:
            return json.loads(data.decode("utf-8"))
        except (json.JSONDecodeError, UnicodeDecodeError) as e:
            print(f"    JSON decode error: {e}")
            return None

    @staticmethod
    def jitter_sleep(base_seconds: float, jitter_range: float = 0.3) -> None:
        """Sleep for base_seconds ± jitter_range fraction.

        Breaks mechanical timing patterns. Default ±30% jitter.

        Args:
            base_seconds: Base sleep duration.
            jitter_range: Fraction of base to randomize (0.3 = ±30%).
        """
        jitter = base_seconds * jitter_range * (2 * random.random() - 1)
        actual = base_seconds + jitter
        time.sleep(actual)
