"""CI-1T low-level HTTP client.

Sync client using httpx. Handles auth, Q0.16 conversion, and request/response
serialization. All methods accept floats (0.0-1.0) or raw Q0.16 ints (0-65535).
"""

from __future__ import annotations

import logging
import os
import time
from typing import Any

import httpx

logger = logging.getLogger("ci1t")

from .types import (
    EvaluateResult,
    FleetEvaluateResult,
    FleetSnapshot,
    RoundResult,
    SessionInfo,
    to_q16,
)

DEFAULT_BASE_URL = "https://collapseindex.org/api"
DEFAULT_TIMEOUT = 30.0


class Client:
    """Sync HTTP client for the CI-1T stability engine.

    Usage::

        import ci1t

        client = ci1t.Client()  # reads CI1T_API_KEY env var
        result = client.evaluate([0.8, 0.85, 0.79])
        print(result.episodes[0].status)  # "Stable"

    Args:
        api_key: CI-1T API key. Falls back to CI1T_API_KEY env var.
        base_url: API base URL. Falls back to CI1T_API_URL env var.
        timeout: Request timeout in seconds.
    """

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str | None = None,
        timeout: float = DEFAULT_TIMEOUT,
    ) -> None:
        self.api_key = api_key or os.getenv("CI1T_API_KEY", "")
        self.base_url = (base_url or os.getenv("CI1T_API_URL", DEFAULT_BASE_URL)).rstrip("/")
        self.timeout = timeout

        if not self.api_key:
            raise ValueError(
                "API key required. Set CI1T_API_KEY env var or pass api_key= to Client(). "
                "Get one at https://collapseindex.org/dashboard"
            )

        self._http = httpx.Client(
            base_url=self.base_url,
            headers={
                "X-API-Key": self.api_key,
                "Content-Type": "application/json",
            },
            timeout=self.timeout,
        )

    def close(self) -> None:
        """Close the underlying HTTP connection pool."""
        self._http.close()

    def __enter__(self) -> Client:
        return self

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

    # ── Single evaluate ──

    def evaluate(
        self,
        scores: list[float | int],
        config: dict[str, Any] | None = None,
    ) -> EvaluateResult:
        """Evaluate prediction scores for stability.

        Scores are grouped into episodes of 3 (configurable via config.n).
        Each episode produces a Collapse Index (CI) and Authority Level (AL).

        Args:
            scores: Prediction scores. Accepts floats (0.0-1.0) or Q0.16
                ints (0-65535). Minimum 3 scores for one episode.
            config: Optional engine config overrides (advanced).

        Returns:
            EvaluateResult with per-episode CI/AL/warn/fault data.

        Raises:
            ValueError: If fewer than 3 scores or scores out of range.
            httpx.HTTPStatusError: On API errors.
        """
        q16_scores = [to_q16(s) for s in scores]
        if len(q16_scores) < 3:
            raise ValueError(f"Need at least 3 scores for one episode, got {len(q16_scores)}.")

        body: dict[str, Any] = {"scores": q16_scores}
        if config:
            body["config"] = config

        logger.debug("POST /evaluate (%d scores)", len(q16_scores))
        t0 = time.monotonic()
        resp = self._http.post("/evaluate", json=body)
        elapsed_ms = (time.monotonic() - t0) * 1000
        resp.raise_for_status()
        result = EvaluateResult.from_dict(resp.json())
        logger.info(
            "evaluate: %d episodes, %.0fms, %d credits left",
            result.episode_count, elapsed_ms, result.credits_remaining,
        )
        return result

    # ── Fleet evaluate (stateless, one-shot) ──

    def fleet_evaluate(
        self,
        nodes: dict[str, list[float | int]] | list[list[float | int]],
        config: dict[str, Any] | None = None,
    ) -> FleetEvaluateResult:
        """Evaluate multiple nodes in a single fleet round.

        Compares nodes against each other for ghost detection.
        Each node needs at least 3 scores.

        Args:
            nodes: Per-node scores. Either:
                - dict mapping node names to score lists
                - list of score lists (anonymous nodes)
            config: Optional fleet config overrides.

        Returns:
            FleetEvaluateResult with per-node CI/AL + ghost flags.
        """
        if isinstance(nodes, dict):
            node_lists = list(nodes.values())
        else:
            node_lists = nodes

        if not node_lists:
            raise ValueError("Need at least one node.")

        q16_nodes = [[to_q16(s) for s in node] for node in node_lists]
        for i, node in enumerate(q16_nodes):
            if len(node) < 3:
                raise ValueError(f"Node {i} needs at least 3 scores, got {len(node)}.")

        body: dict[str, Any] = {"nodes": q16_nodes}
        if config:
            body["config"] = config

        logger.debug("POST /fleet-evaluate (%d nodes)", len(q16_nodes))
        t0 = time.monotonic()
        resp = self._http.post("/fleet-evaluate", json=body)
        elapsed_ms = (time.monotonic() - t0) * 1000
        resp.raise_for_status()
        result = FleetEvaluateResult.from_dict(resp.json())
        logger.info(
            "fleet_evaluate: %d nodes, %d ghosts, %.0fms",
            len(result.snapshot.nodes), result.snapshot.ghost_confirmed_count, elapsed_ms,
        )
        return result

    # ── Fleet sessions (persistent, stateful) ──

    def session_create(
        self,
        node_count: int,
        node_names: list[str] | None = None,
    ) -> SessionInfo:
        """Create a persistent fleet monitoring session.

        Args:
            node_count: Number of nodes to monitor.
            node_names: Optional names for each node.

        Returns:
            SessionInfo with session_id and node_count.
        """
        body: dict[str, Any] = {"action": "create", "node_count": node_count}
        if node_names:
            body["node_names"] = node_names
        logger.debug("POST /fleet-session create (%d nodes)", node_count)
        resp = self._http.post("/fleet-session", json=body)
        resp.raise_for_status()
        info = SessionInfo.from_dict(resp.json())
        logger.info("session_create: %s (%d nodes)", info.session_id, info.node_count)
        return info

    def session_round(
        self,
        session_id: str,
        scores: dict[str, list[float | int]] | list[list[float | int]],
    ) -> RoundResult:
        """Push a scoring round to a fleet session.

        Args:
            session_id: Session ID from session_create().
            scores: Per-node score arrays for this round.

        Returns:
            RoundResult with snapshot and round number.
        """
        if isinstance(scores, dict):
            score_lists = list(scores.values())
        else:
            score_lists = scores

        q16_scores = [[to_q16(s) for s in node] for node in score_lists]

        body: dict[str, Any] = {
            "action": "round",
            "session_id": session_id,
            "scores": q16_scores,
        }
        logger.debug("POST /fleet-session round %s", session_id)
        t0 = time.monotonic()
        resp = self._http.post("/fleet-session", json=body)
        elapsed_ms = (time.monotonic() - t0) * 1000
        resp.raise_for_status()
        result = RoundResult.from_dict(resp.json())
        logger.info(
            "session_round: %s round %d, %d ghosts, %.0fms",
            session_id, result.round, result.snapshot.ghost_confirmed_count, elapsed_ms,
        )
        return result

    def session_state(self, session_id: str) -> FleetSnapshot:
        """Get the current state of a fleet session.

        Args:
            session_id: Session ID from session_create().

        Returns:
            FleetSnapshot with latest per-node results.
        """
        body = {"action": "state", "session_id": session_id}
        resp = self._http.post("/fleet-session", json=body)
        resp.raise_for_status()
        data = resp.json()
        # State response wraps the snapshot differently depending on proxy version
        snapshot_data = data.get("snapshot", data)
        return FleetSnapshot.from_dict(snapshot_data)

    def session_delete(self, session_id: str) -> dict[str, Any]:
        """Delete a fleet session.

        Args:
            session_id: Session ID to delete.

        Returns:
            Confirmation dict from API.
        """
        body = {"action": "delete", "session_id": session_id}
        resp = self._http.post("/fleet-session", json=body)
        resp.raise_for_status()
        logger.info("session_delete: %s", session_id)
        return resp.json()

    def session_list(self) -> list[SessionInfo]:
        """List all active fleet sessions.

        Returns:
            List of SessionInfo for each active session.
        """
        body = {"action": "list"}
        resp = self._http.post("/fleet-session", json=body)
        resp.raise_for_status()
        data = resp.json()
        sessions = data.get("sessions", [])
        return [SessionInfo.from_dict(s) for s in sessions]

    def health(self) -> dict[str, Any]:
        """Check API health.

        Returns:
            Health status dict with version info.
        """
        # Health endpoint lives at the root, not under /api
        base = self.base_url.replace("/api", "", 1) if self.base_url.endswith("/api") else self.base_url
        resp = httpx.get(f"{base}/health", timeout=self.timeout)
        resp.raise_for_status()
        return resp.json()
