"""CrowdStrike Humio API client."""
import json
import logging
import time
from typing import Any, Generator

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

from .constants import (
    API_BASE,
    API_TIMEOUT,
    MAX_RETRIES,
    MAX_WAIT_MINUTES,
    PAGINATION_LIMIT,
    POLL_INTERVAL_SEC,
    SEARCH_JOB_LIMIT,
    SSL_RETRIES,
    TOKEN_REFRESH_BUFFER_SECONDS,
)

logger = logging.getLogger(__name__)


class CrowdStrikeClient:
    """Client for CrowdStrike Humio API."""

    def __init__(self, client_id: str, client_secret: str):
        if not client_id or not client_secret:
            raise ValueError("client_id and client_secret are required")

        self._client_id = client_id
        self._client_secret = client_secret
        self._token: str | None = None
        self._token_expires_at: float = 0
        self._session = self._create_session()
        self._refresh_token()

    def _create_session(self) -> requests.Session:
        """Create session with retry configuration."""
        session = requests.Session()
        retry = Retry(
            total=MAX_RETRIES,
            backoff_factor=1,
            status_forcelist=[429, 500, 502, 503, 504],
        )
        session.mount("https://", HTTPAdapter(max_retries=retry))
        return session

    def _refresh_token(self) -> None:
        """Refresh OAuth token."""
        response = self._session.post(
            f"{API_BASE}/oauth2/token",
            headers={"Content-Type": "application/x-www-form-urlencoded"},
            data={"client_id": self._client_id, "client_secret": self._client_secret},
            timeout=API_TIMEOUT,
        )
        response.raise_for_status()
        data = response.json()
        self._token = data["access_token"]
        expires_in = data.get("expires_in", 1800)
        self._token_expires_at = time.time() + expires_in - TOKEN_REFRESH_BUFFER_SECONDS
        logger.info(f"Authenticated (token expires in {expires_in}s)")

    def _headers(self) -> dict[str, str]:
        """Get headers with fresh token."""
        if time.time() >= self._token_expires_at:
            self._refresh_token()
        return {"Authorization": f"Bearer {self._token}", "Content-Type": "application/json"}

    def _request_with_retry(self, method: str, url: str, **kwargs: Any) -> requests.Response:
        """Make request with SSL/connection retry.

        The session's urllib3 Retry handles 429/5xx, but SSL handshake and
        connection-reset errors need explicit retry.
        """
        for attempt in range(1, SSL_RETRIES + 1):
            try:
                response = getattr(self._session, method)(
                    url, headers=self._headers(), timeout=API_TIMEOUT, **kwargs
                )
                response.raise_for_status()
                return response
            except (requests.exceptions.SSLError, requests.exceptions.ConnectionError) as e:
                if attempt >= SSL_RETRIES:
                    logger.error(f"Connection error after {SSL_RETRIES} retries: {e}")
                    raise
                sleep_time = 2 ** attempt
                logger.warning(f"Connection error (attempt {attempt}/{SSL_RETRIES}), retrying in {sleep_time}s")
                time.sleep(sleep_time)
        raise RuntimeError("Request failed after retries")  # unreachable, satisfies type checker

    def search(
        self, query: str, start_ts: int, end_ts: int, repo: str
    ) -> Generator[dict[str, Any], None, None]:
        """Execute search query and yield results with pagination.

        Starts a search job, polls until completion, then paginates through
        all results.

        Args:
            query: Humio query string
            start_ts: Start timestamp in epoch milliseconds
            end_ts: End timestamp in epoch milliseconds
            repo: Repository to search

        Yields:
            Individual event dicts
        """
        # Start search job
        url = f"{API_BASE}/humio/api/v1/repositories/{repo}/queryjobs/"
        response = self._request_with_retry(
            "post", url,
            json={"queryString": query, "start": start_ts, "end": end_ts, "limit": SEARCH_JOB_LIMIT},
        )
        job_id = response.json()["id"]
        logger.info(f"Started search job: {job_id}")

        # Poll until done, then paginate results
        job_url = f"{url}{job_id}"
        self._poll_until_done(job_id, job_url)
        yield from self._fetch_results(job_id, job_url)

    def _poll_until_done(self, job_id: str, job_url: str) -> None:
        """Poll until search job completes."""
        max_iterations = (MAX_WAIT_MINUTES * 60) // POLL_INTERVAL_SEC

        for _ in range(max_iterations):
            try:
                resp = self._request_with_retry("get", job_url)
                data = resp.json()
            except requests.exceptions.HTTPError as e:
                body = e.response.text[:500] if e.response.text else "empty"
                logger.exception(f"HTTP {e.response.status_code} polling job {job_id}: {body}")
                raise RuntimeError(f"Failed to poll search job: {e}") from e

            if data.get("done"):
                logger.info(f"Search job {job_id} completed")
                return

            if data.get("cancelled"):
                raise RuntimeError(f"Search was cancelled: {json.dumps(data)}")

            time.sleep(POLL_INTERVAL_SEC)

        raise RuntimeError(f"Search timed out after {MAX_WAIT_MINUTES} minutes")

    def _fetch_results(self, job_id: str, job_url: str) -> Generator[dict[str, Any], None, None]:
        """Fetch paginated results from completed job.

        Args:
            job_id: Search job ID (for logging)
            job_url: Full URL for the search job

        Yields:
            Individual event dicts
        """
        offset = 0
        while True:
            params: dict[str, Any] = {"paginationLimit": PAGINATION_LIMIT}
            if offset > 0:
                params["paginationOffset"] = offset

            resp = self._request_with_retry("get", job_url, params=params)

            events = resp.json().get("events", [])
            if not events:
                logger.info(f"No more events at offset {offset}")
                break

            logger.info(f"Fetched {len(events)} events (offset: {offset})")
            yield from events

            offset += PAGINATION_LIMIT
