"""HTTP client with browser simulation and manual redirect handling.

Mirrors the Rust GencatClient:
- Cookie jar maintained across requests (via httpx.Client)
- Redirects are followed manually so each hop can be inspected
- Rotating User-Agent headers
- Realistic browser headers (Accept, Accept-Language, Sec-Fetch-*, etc.)
- Exponential back-off retry for transient errors
"""

from __future__ import annotations

import logging
import random
import time
from typing import Optional
from urllib.parse import urljoin, urlparse

import httpx

from .errors import (
    HttpError,
    NetworkError,
    RateLimitError,
    TooManyRedirectsError,
    WafBlockedError,
)
from .types import GencatConfig

logger = logging.getLogger(__name__)

_WAF_INDICATORS = frozenset(
    [
        "cloudflare",
        "cf-ray",
        "x-sucuri-id",
        "x-denied-reason",
        "x-wzws-requested-method",
        "x-firewall",
        "blocked",
        "security",
    ]
)

_WAF_STATUS_CODES = frozenset([403, 406, 409, 429, 444, 499, 503])


class GencatClient:
    """Synchronous HTTP client for the Gencat SAML flow."""

    def __init__(self, config: GencatConfig):
        self.config = config
        self._ua_index = 0
        self._client = httpx.Client(
            follow_redirects=False,  # Manual redirect handling
            timeout=config.request_timeout.total_seconds(),
            verify=True,
        )

    def close(self) -> None:
        self._client.close()

    def __enter__(self) -> "GencatClient":
        return self

    def __exit__(self, *_) -> None:
        self.close()

    # ------------------------------------------------------------------
    # User-agent rotation
    # ------------------------------------------------------------------

    def _next_ua(self) -> str:
        ua = self.config.user_agents[self._ua_index % len(self.config.user_agents)]
        self._ua_index += 1
        return ua

    # ------------------------------------------------------------------
    # Browser header generation
    # ------------------------------------------------------------------

    def _browser_headers(self, ua: Optional[str] = None) -> dict[str, str]:
        if ua is None:
            ua = self._next_ua()
        headers: dict[str, str] = {
            "User-Agent": ua,
            "Accept": (
                "text/html,application/xhtml+xml,application/xml;q=0.9,"
                "image/avif,image/webp,image/apng,*/*;q=0.8,"
                "application/signed-exchange;v=b3;q=0.7"
            ),
            "Accept-Language": "ca-ES,ca;q=0.9,es;q=0.8,en;q=0.7",
            "Accept-Encoding": "gzip, deflate, br",
            "Cache-Control": "max-age=0",
            "Connection": "keep-alive",
            "Upgrade-Insecure-Requests": "1",
            "Sec-Fetch-Dest": "document",
            "Sec-Fetch-Mode": "navigate",
            "Sec-Fetch-Site": "same-origin",
            "Sec-Fetch-User": "?1",
        }
        if "Chrome" in ua:
            headers["sec-ch-ua"] = (
                '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"'
            )
            headers["sec-ch-ua-mobile"] = "?0"
            if "Linux" in ua:
                headers["sec-ch-ua-platform"] = '"Linux"'
            elif "Windows" in ua:
                headers["sec-ch-ua-platform"] = '"Windows"'
            elif "Macintosh" in ua:
                headers["sec-ch-ua-platform"] = '"macOS"'
        if random.random() < 0.5:
            headers["DNT"] = "1"
        return headers

    # ------------------------------------------------------------------
    # Request delay
    # ------------------------------------------------------------------

    def _apply_delay(self) -> None:
        base_ms = self.config.request_delay.total_seconds() * 1000
        jitter = random.uniform(0.5, 1.5)
        delay_ms = base_ms * jitter
        if delay_ms > 0:
            time.sleep(delay_ms / 1000)

    # ------------------------------------------------------------------
    # WAF / rate-limit checks
    # ------------------------------------------------------------------

    def _check_waf(self, response: httpx.Response) -> None:
        code = response.status_code
        if code in _WAF_STATUS_CODES:
            header_str = " ".join(
                f"{k}: {v}" for k, v in response.headers.items()
            ).lower()
            if any(ind in header_str for ind in _WAF_INDICATORS):
                raise WafBlockedError(code)
        if code in (429, 503):
            retry_after_raw = response.headers.get("retry-after")
            retry_after = (
                int(retry_after_raw)
                if retry_after_raw and retry_after_raw.isdigit()
                else None
            )
            raise RateLimitError(retry_after)

    # ------------------------------------------------------------------
    # Low-level send with retry
    # ------------------------------------------------------------------

    def _send(self, request: httpx.Request, attempt: int = 0) -> httpx.Response:
        try:
            response = self._client.send(request)
        except httpx.TimeoutException as exc:
            if attempt < self.config.max_retries:
                self._backoff(attempt)
                new_req = self._client.build_request(
                    request.method, request.url, headers=request.headers
                )
                return self._send(new_req, attempt + 1)
            raise NetworkError(f"Request timed out: {request.url}", exc) from exc
        except httpx.RequestError as exc:
            if attempt < self.config.max_retries:
                self._backoff(attempt)
                new_req = self._client.build_request(
                    request.method, request.url, headers=request.headers
                )
                return self._send(new_req, attempt + 1)
            raise NetworkError(f"Request failed: {exc}", exc) from exc

        self._check_waf(response)

        status = response.status_code
        if status >= 400:
            raise HttpError(status, f"for {request.url}")

        return response

    def _backoff(self, attempt: int) -> None:
        base_ms = 1000
        max_ms = 30_000
        delay = min(base_ms * (2**attempt), max_ms)
        jitter = random.uniform(0.75, 1.25)
        time.sleep(delay * jitter / 1000)

    # ------------------------------------------------------------------
    # URL resolution
    # ------------------------------------------------------------------

    @staticmethod
    def resolve_url(base: str, location: str) -> str:
        if location.startswith("http://") or location.startswith("https://"):
            return location
        return urljoin(base, location)

    # ------------------------------------------------------------------
    # High-level helpers (mirror Rust GencatClient)
    # ------------------------------------------------------------------

    def get_following_redirects(self, url: str) -> tuple[str, str]:
        """GET url, follow all redirects, return (final_url, body)."""
        current_url = url
        for _ in range(20):
            self._apply_delay()
            headers = self._browser_headers()
            req = self._client.build_request("GET", current_url, headers=headers)
            resp = self._send(req)
            if resp.is_redirect:
                location = resp.headers.get("location", "")
                current_url = self.resolve_url(current_url, location)
                logger.debug("GET redirect → %s", current_url)
                continue
            return current_url, resp.text
        raise TooManyRedirectsError(f"Too many redirects from {url}")

    def post_form_following_redirects(
        self,
        url: str,
        form_data: dict[str, str],
        extra_headers: Optional[dict[str, str]] = None,
    ) -> tuple[str, str]:
        """POST url-encoded form, follow redirects, return (final_url, body)."""
        current_url = url
        current_data: Optional[dict[str, str]] = form_data
        method = "POST"

        for _ in range(20):
            self._apply_delay()
            headers = self._browser_headers()
            if extra_headers:
                headers.update(extra_headers)

            if method == "POST" and current_data is not None:
                req = self._client.build_request(
                    "POST",
                    current_url,
                    data=current_data,
                    headers=headers,
                )
            else:
                req = self._client.build_request("GET", current_url, headers=headers)

            resp = self._send(req)
            logger.debug("post_form status=%s url=%s", resp.status_code, current_url)

            if resp.is_redirect:
                location = resp.headers.get("location", "")
                prev = current_url
                current_url = self.resolve_url(current_url, location)
                method = "GET"
                current_data = None
                logger.debug("redirect %s → %s", prev, current_url)
                continue

            return current_url, resp.text

        raise TooManyRedirectsError(f"Too many redirects from {url}")

    def post_json(self, url: str, json_body: str) -> str:
        """POST raw JSON, return response text (no redirect following)."""
        ua = self._next_ua()
        headers = {
            "User-Agent": ua,
            "Content-Type": "application/json",
            "Accept": "application/json, text/javascript, */*; q=0.01",
            "Accept-Language": "ca-ES,ca;q=0.9,es;q=0.8,en;q=0.7",
            "Origin": "https://login.microsoftonline.com",
            "Referer": "https://login.microsoftonline.com/",
        }
        self._apply_delay()
        req = self._client.build_request(
            "POST", url, content=json_body, headers=headers
        )
        resp = self._send(req)
        return resp.text
