"""Cookie pool (FETCH-04).

Per design doc §4.2: after a browser successfully bypasses a JS challenge,
the resulting Cookie set is written to Valkey under key `cookie:<host>` with
TTL 4 h. Subsequent same-host httpx requests read that set and skip the
browser entirely — the cheap path succeeds because the challenge dropped a
session cookie the WAF now honors.

Backend selection:
- `VALKEY_URL` unset/empty  → InMemoryCookieStore (dev + tests)
- `VALKEY_URL` set          → ValkeyCookieStore (production)

Wire format: JSON dict {cookie_name: cookie_value}. Only simple name/value
pairs are stored; we don't preserve domain/path flags because every cookie
in the set was issued by the same host and we replay them only on that host.
"""
from __future__ import annotations

import json
import logging
import time
from typing import Protocol

from govcrawler.settings import get_settings

log = logging.getLogger(__name__)


def _key(host: str) -> str:
    return f"cookie:{host.lower()}"


class CookieStore(Protocol):
    """Minimal interface the fetcher needs."""

    def get(self, host: str) -> dict[str, str] | None: ...
    def set(self, host: str, cookies: dict[str, str], *, ttl_s: int | None = None) -> None: ...
    def invalidate(self, host: str) -> None: ...


class InMemoryCookieStore:
    """Dict-backed with TTL tracking. Safe for a single-process dev setup."""

    def __init__(self) -> None:
        self._data: dict[str, tuple[dict[str, str], float]] = {}

    def get(self, host: str) -> dict[str, str] | None:
        entry = self._data.get(_key(host))
        if entry is None:
            return None
        cookies, expires_at = entry
        if expires_at and time.time() > expires_at:
            self._data.pop(_key(host), None)
            return None
        return dict(cookies)   # defensive copy

    def set(self, host: str, cookies: dict[str, str], *, ttl_s: int | None = None) -> None:
        if not cookies:
            return
        ttl = ttl_s if ttl_s is not None else get_settings().cookie_ttl_s
        expires_at = time.time() + ttl if ttl > 0 else 0.0
        self._data[_key(host)] = (dict(cookies), expires_at)

    def invalidate(self, host: str) -> None:
        self._data.pop(_key(host), None)


class ValkeyCookieStore:
    """Redis-protocol client around a Valkey server.

    Uses redis-py (MIT) — protocol-compatible with Valkey. Fails open: if the
    backend is unreachable, operations log and return None rather than raising,
    so a broken cookie pool never takes the crawler down.
    """

    def __init__(self, url: str) -> None:
        import redis  # lazy import — only needed when backend configured

        self._client = redis.Redis.from_url(url, decode_responses=True)

    def get(self, host: str) -> dict[str, str] | None:
        try:
            raw = self._client.get(_key(host))
        except Exception as e:
            log.warning("ValkeyCookieStore.get failed host=%s err=%s", host, e)
            return None
        if raw is None:
            return None
        try:
            return dict(json.loads(raw))
        except Exception as e:
            log.warning("ValkeyCookieStore.get parse failed host=%s err=%s", host, e)
            return None

    def set(self, host: str, cookies: dict[str, str], *, ttl_s: int | None = None) -> None:
        if not cookies:
            return
        ttl = ttl_s if ttl_s is not None else get_settings().cookie_ttl_s
        try:
            self._client.set(_key(host), json.dumps(cookies), ex=ttl if ttl > 0 else None)
        except Exception as e:
            log.warning("ValkeyCookieStore.set failed host=%s err=%s", host, e)

    def invalidate(self, host: str) -> None:
        try:
            self._client.delete(_key(host))
        except Exception as e:
            log.warning("ValkeyCookieStore.invalidate failed host=%s err=%s", host, e)


_default: CookieStore | None = None


def get_default_store() -> CookieStore:
    """Singleton; chosen by VALKEY_URL env."""
    global _default
    if _default is not None:
        return _default
    url = (get_settings().valkey_url or "").strip()
    if url:
        try:
            _default = ValkeyCookieStore(url)
            log.info("cookie pool: Valkey backend at %s", url)
        except Exception as e:
            log.warning("Valkey init failed (%s) — falling back to in-memory", e)
            _default = InMemoryCookieStore()
    else:
        _default = InMemoryCookieStore()
        log.info("cookie pool: in-memory backend (set VALKEY_URL for production)")
    return _default


def reset_default_store() -> None:
    """Tests rely on this to force re-selection of the backend."""
    global _default
    _default = None
