"""HTTP transport for forwarding events to the Sluice API.

Uses urllib3 (no dependency on ``requests``). Retries on 429/5xx with
exponential backoff and jitter. Bearer token auth via API key.

The transport is designed to be used from a background thread and never
raises exceptions that could crash the Celery worker.
"""

from __future__ import annotations

import json
import logging
import random
import threading
from typing import Any

import urllib3

logger = logging.getLogger("sluice")

_DEFAULT_BASE_URL = "https://sluice.sh"
_INGEST_PATH = "/api/ingest"

# Retry configuration.
_MAX_RETRIES = 5
_INITIAL_BACKOFF_S = 1.0
_MAX_BACKOFF_S = 30.0
_JITTER_FACTOR = 0.2  # ±20%

# Retryable HTTP status codes.
_RETRYABLE_STATUSES: frozenset[int] = frozenset({429, 500, 502, 503, 504})

# Connection pool settings.
_POOL_MAXSIZE = 2
_TIMEOUT_CONNECT_S = 5.0
_TIMEOUT_READ_S = 10.0


class SluiceTransport:
    """HTTP client for sending batches of events to the Sluice ingest API.

    Thread-safe. A single instance should be shared across the SDK.
    """

    def __init__(
        self,
        api_key: str,
        *,
        base_url: str = _DEFAULT_BASE_URL,
        connection_id: str = "",
        max_retries: int = _MAX_RETRIES,
        cancel_event: threading.Event | None = None,
        timeout_connect: float = _TIMEOUT_CONNECT_S,
        timeout_read: float = _TIMEOUT_READ_S,
    ) -> None:
        self._api_key = api_key
        self._connection_id = connection_id
        self._ingest_url = base_url.rstrip("/") + _INGEST_PATH
        self._max_retries = max_retries
        # External cancel event — when set, retry sleeps are interrupted.
        self._cancel = cancel_event or threading.Event()

        pool_cls: type[urllib3.HTTPConnectionPool] = (
            urllib3.HTTPConnectionPool
            if base_url.startswith("http://")
            else urllib3.HTTPSConnectionPool
        )
        pool_kwargs: dict[str, Any] = {
            "host": self._parse_host(base_url),
            "port": self._parse_port(base_url),
            "maxsize": _POOL_MAXSIZE,
            "timeout": urllib3.Timeout(
                connect=timeout_connect,
                read=timeout_read,
            ),
            "retries": False,  # We handle retries ourselves.
        }
        if pool_cls is urllib3.HTTPSConnectionPool:
            pool_kwargs["cert_reqs"] = "CERT_REQUIRED"
        self._pool: urllib3.HTTPConnectionPool = pool_cls(**pool_kwargs)

        self._headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "User-Agent": "sluice-sdk/0.1.0",
        }
        self._closed = False

    def send_batch(self, events: list[dict[str, Any]]) -> bool:
        """Send a batch of normalized events to the Sluice API.

        Returns True if the batch was accepted (2xx), False otherwise.
        Retries on 429/5xx with exponential backoff. Never raises.
        """
        if self._closed:
            return False

        if not events:
            return True

        # Inject connectionId into each event (required by ingestEventSchema).
        # Events from _events.py don't include connectionId — it's a
        # transport-level concern tied to the API key / connection config.
        # Shallow-copy to avoid mutating the caller's dicts.
        if self._connection_id:
            enriched = [{**e, "connectionId": self._connection_id} for e in events]
        else:
            enriched = events

        payload = json.dumps({"events": enriched}).encode("utf-8")

        backoff = _INITIAL_BACKOFF_S
        for attempt in range(1, self._max_retries + 1):
            try:
                resp = self._pool.urlopen(
                    "POST",
                    _INGEST_PATH,
                    body=payload,
                    headers=self._headers,
                )

                if 200 <= resp.status < 300:
                    return True

                if resp.status in _RETRYABLE_STATUSES:
                    # Check Retry-After header for 429.
                    retry_after = self._parse_retry_after(resp)
                    wait = retry_after if retry_after else backoff

                    if attempt < self._max_retries:
                        logger.warning(
                            "[sluice] Ingest API returned %d. "
                            "Retrying in %.1fs (attempt %d/%d).",
                            resp.status,
                            wait,
                            attempt,
                            self._max_retries,
                        )
                        if self._cancel.wait(timeout=wait):
                            return False  # Cancelled during backoff.
                        backoff = _next_backoff(backoff)
                        continue

                    logger.error(
                        "[sluice] Ingest API returned %d after %d attempts. "
                        "Batch of %d events dropped. "
                        "See: https://docs.sluice.sh/troubleshooting/ingest",
                        resp.status,
                        self._max_retries,
                        len(events),
                    )
                    return False

                # Non-retryable error (4xx except 429).
                logger.error(
                    "[sluice] Ingest API returned %d. "
                    "Batch of %d events dropped. "
                    "See: https://docs.sluice.sh/troubleshooting/ingest",
                    resp.status,
                    len(events),
                )
                return False

            except Exception:
                if attempt < self._max_retries:
                    logger.warning(
                        "[sluice] Network error sending events. "
                        "Retrying in %.1fs (attempt %d/%d).",
                        backoff,
                        attempt,
                        self._max_retries,
                        exc_info=True,
                    )
                    if self._cancel.wait(timeout=backoff):
                        return False  # Cancelled during backoff.
                    backoff = _next_backoff(backoff)
                else:
                    logger.error(
                        "[sluice] Network error after %d attempts. "
                        "Batch of %d events dropped. "
                        "See: https://docs.sluice.sh/troubleshooting/network",
                        self._max_retries,
                        len(events),
                        exc_info=True,
                    )
                    return False

        return False  # pragma: no cover — unreachable, satisfies mypy

    def close(self) -> None:
        """Close the connection pool. No more sends after this."""
        self._closed = True
        try:
            self._pool.close()
        except Exception:
            logger.debug("[sluice] Error closing transport pool.", exc_info=True)

    @staticmethod
    def _parse_host(url: str) -> str:
        """Extract hostname from a URL."""
        # Strip scheme.
        without_scheme = url.split("://", 1)[-1]
        # Strip path.
        host_port = without_scheme.split("/", 1)[0]
        # Strip port.
        return host_port.rsplit(":", 1)[0]

    @staticmethod
    def _parse_port(url: str) -> int:
        """Extract port from a URL, defaulting to 443 for https."""
        without_scheme = url.split("://", 1)[-1]
        host_port = without_scheme.split("/", 1)[0]
        parts = host_port.rsplit(":", 1)
        if len(parts) == 2:
            try:
                return int(parts[1])
            except ValueError:
                pass
        if url.startswith("http://"):
            return 80
        return 443

    @staticmethod
    def _parse_retry_after(resp: urllib3.BaseHTTPResponse) -> float | None:
        """Parse the Retry-After header if present."""
        value = resp.headers.get("Retry-After")
        if value is None:
            return None
        try:
            return float(value)
        except (ValueError, TypeError):
            return None


def _next_backoff(current: float) -> float:
    """Calculate the next backoff with jitter."""
    next_val = min(current * 2, _MAX_BACKOFF_S)
    jitter = next_val * _JITTER_FACTOR * (2 * random.random() - 1)
    return max(0.1, next_val + jitter)
