# === OPTIMIZER COMPONENTS (Agent #5) ===
# Retry logic, exponential backoff, adaptive rate limiting, response caching

import time
import hashlib
import json
import os
import logging
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
from urllib.parse import urlparse
from http.client import RemoteDisconnected

logger = logging.getLogger("braniac")

# --- Response Cache ---
class ResponseCache:
    """Disk-backed HTTP response cache using stdlib only."""
    def __init__(self, cache_dir=".braniac_cache"):
        self.cache_dir = cache_dir
        os.makedirs(cache_dir, exist_ok=True)

    def _key(self, url: str) -> str:
        return hashlib.sha256(url.encode()).hexdigest()

    def get(self, url: str) -> str | None:
        path = os.path.join(self.cache_dir, self._key(url))
        if os.path.exists(path):
            age = time.time() - os.path.getmtime(path)
            if age < 3600:  # 1hr TTL
                with open(path, "r", encoding="utf-8") as f:
                    logger.debug(f"Cache HIT: {url}")
                    return f.read()
        return None

    def put(self, url: str, content: str):
        path = os.path.join(self.cache_dir, self._key(url))
        with open(path, "w", encoding="utf-8") as f:
            f.write(content)

    def clear(self):
        for f in os.listdir(self.cache_dir):
            os.remove(os.path.join(self.cache_dir, f))


# --- Adaptive Rate Limiter ---
class RateLimiter:
    """Per-domain adaptive rate limiting. Slows down on errors, speeds up on success."""
    def __init__(self):
        self._delays: dict[str, float] = {}  # domain -> current delay
        self._last_req: dict[str, float] = {}
        self.min_delay = 0.5
        self.max_delay = 30.0

    def wait(self, url: str):
        domain = urlparse(url).netloc
        delay = self._delays.get(domain, self.min_delay)
        last = self._last_req.get(domain, 0)
        elapsed = time.time() - last
        if elapsed < delay:
            sleep_time = delay - elapsed
            logger.debug(f"Rate limit: sleeping {sleep_time:.1f}s for {domain}")
            time.sleep(sleep_time)
        self._last_req[domain] = time.time()

    def report_success(self, url: str):
        domain = urlparse(url).netloc
        cur = self._delays.get(domain, self.min_delay)
        self._delays[domain] = max(self.min_delay, cur * 0.8)  # speed up 20%

    def report_error(self, url: str):
        domain = urlparse(url).netloc
        cur = self._delays.get(domain, self.min_delay)
        self._delays[domain] = min(self.max_delay, cur * 2.0)  # double on error


# --- Retry with Exponential Backoff ---
def fetch_with_retry(
    url: str,
    cache: ResponseCache,
    limiter: RateLimiter,
    max_retries: int = 3,
    base_delay: float = 1.0,
    timeout: int = 15,
) -> str:
    """Fetch URL with caching, rate limiting, and exponential backoff retry."""
    # Check cache first
    cached = cache.get(url)
    if cached is not None:
        return cached

    headers = {"User-Agent": "Braniac/1.0 (educational scraper)"}
    last_exc = None

    for attempt in range(max_retries + 1):
        limiter.wait(url)
        try:
            req = Request(url, headers=headers)
            with urlopen(req, timeout=timeout) as resp:
                content = resp.read().decode("utf-8", errors="replace")
            limiter.report_success(url)
            cache.put(url, content)
            logger.info(f"Fetched: {url} (attempt {attempt+1})")
            return content
        except (HTTPError, URLError, RemoteDisconnected, TimeoutError, OSError) as e:
            last_exc = e
            limiter.report_error(url)
            if attempt < max_retries:
                delay = base_delay * (2 ** attempt)  # exponential backoff
                logger.warning(f"Retry {attempt+1}/{max_retries} for {url} in {delay:.1f}s: {e}")
                time.sleep(delay)
            else:
                logger.error(f"Failed after {max_retries+1} attempts: {url}")

    raise ConnectionError(f"Failed to fetch {url}: {last_exc}")


# --- Benchmark / Compete ---
def benchmark_fetch(url: str, runs: int = 5) -> dict:
    """Benchmark fetching performance. Returns timing stats."""
    cache = ResponseCache()
    limiter = RateLimiter()
    times = []
    errors = 0

    cache.clear()  # fair benchmark — no cache advantage

    for i in range(runs):
        start = time.perf_counter()
        try:
            fetch_with_retry(url, cache, limiter, max_retries=1)
            elapsed = time.perf_counter() - start
            times.append(elapsed)
        except ConnectionError:
            errors += 1
        cache.clear()  # reset between runs

    return {
        "runs": runs,
        "errors": errors,
        "avg_ms": (sum(times) / len(times) * 1000) if times else 0,
        "min_ms": min(times) * 1000 if times else 0,
        "max_ms": max(times) * 1000 if times else 0,
        "p95_ms": sorted(times)[int(len(times)*0.95)] * 1000 if times else 0,
    }