"""Bandito backend API client.

All backend operations go through this client.
Requires BANDITO_API_KEY. No offline mode — fails if backend is unreachable.
"""

from __future__ import annotations

import os
from typing import Any
from urllib.parse import quote


_DEFAULT_BASE_URL = "https://bandito-api.onrender.com"


def _resolve_bandito_api_key() -> str | None:
    """Resolve BANDITO_API_KEY from env var or config.toml."""
    key = os.environ.get("BANDITO_API_KEY")
    if key:
        return key
    try:
        from ..config import load_config
        config = load_config()
        return config.api_key
    except (FileNotFoundError, OSError):
        return None


class BanditoClient:
    """Client for the Bandito backend API."""

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str | None = None,
    ):
        self._api_key = api_key or _resolve_bandito_api_key()
        if not self._api_key:
            raise RuntimeError(
                "BANDITO_API_KEY is required. "
                "Run: bandito signup"
            )
        self._base_url = self._resolve_base_url(base_url)

    # -- Rubrics --

    def get_rubric(self, project: str) -> dict | None:
        """Fetch rubric for a project. Returns None if not found."""
        resp = self._request("GET", f"/api/v1/rubrics/{quote(project, safe='')}")
        if resp.status_code == 404:
            return None
        self._check(resp)
        return resp.json()

    def save_rubric(self, project: str, content: str) -> dict:
        """Create or update rubric. Returns the rubric dict."""
        resp = self._request(
            "PUT", f"/api/v1/rubrics/{quote(project, safe='')}", json={"content": content}
        )
        self._check(resp)
        return resp.json()

    def delete_rubric(self, project: str) -> None:
        """Delete rubric for a project."""
        resp = self._request("DELETE", f"/api/v1/rubrics/{quote(project, safe='')}")
        self._check(resp)

    def draft_rubric(self, project: str, traces: list[dict]) -> str:
        """Generate a rubric from sample traces via the draft agent.

        Returns the rubric markdown content.
        """
        resp = self._request(
            "POST", "/api/v1/draft/generate",
            json={"project": project, "traces": traces},
            timeout=90.0,
        )
        self._check(resp)
        return resp.json()["content"]

    # -- Evaluations --

    def write_evaluation(self, evaluation: dict) -> dict:
        """Write a single evaluation (upsert). Returns the evaluation dict."""
        resp = self._request("POST", "/api/v1/evaluations", json=evaluation)
        self._check(resp, expected=201)
        return resp.json()

    def list_evaluations(
        self,
        project: str,
        limit: int | None = None,
        offset: int = 0,
        since: str | None = None,
    ) -> dict[str, dict]:
        """List evaluations for a project, keyed by trace_id.

        When limit or since is provided, uses the paginated endpoint.
        Default (no limit/since) returns all evaluations.
        """
        if limit is not None or since is not None:
            params: dict[str, Any] = {"project": project, "limit": limit or 1000, "offset": offset}
            if since is not None:
                params["since"] = since
            resp = self._request(
                "GET",
                "/api/v1/evaluations/list",
                params=params,
            )
            self._check(resp)
            return {ev["trace_id"]: ev for ev in resp.json()["evaluations"]}

        resp = self._request(
            "GET", "/api/v1/evaluations", params={"project": project}
        )
        self._check(resp)
        return resp.json()["evaluations"]

    def get_evaluation(self, trace_id: str) -> dict | None:
        """Get evaluation for a trace. Returns None if not found."""
        resp = self._request("GET", f"/api/v1/evaluations/{quote(trace_id, safe='')}")
        if resp.status_code == 404:
            return None
        self._check(resp)
        return resp.json()

    # -- Calibrations --

    def get_calibration(self, project: str) -> dict | None:
        """Get calibration status. Returns None if never calibrated."""
        resp = self._request("GET", f"/api/v1/calibrations/{quote(project, safe='')}")
        self._check(resp)
        data = resp.json()
        if not data.get("calibrated"):
            return None
        return data

    def run_calibration(
        self, project: str, traces: list[dict]
    ) -> dict:
        """Execute a full calibration run on the backend.

        Args:
            project: Project name.
            traces: List of dicts with trace_id, input, output, human_score.

        Returns:
            CalibrationRunResponse dict with metrics, trace_results, calibration.
        """
        resp = self._request(
            "POST",
            f"/api/v1/calibrations/{quote(project, safe='')}/run",
            json={"traces": traces},
            timeout=300.0,
        )
        self._check(resp)
        return resp.json()

    # -- Calibration Traces --

    def list_calibration_traces(
        self,
        project: str,
        limit: int | None = None,
        offset: int = 0,
    ) -> list[dict]:
        """List per-trace calibration results for a project."""
        params: dict = {}
        if limit is not None:
            params["limit"] = limit
            params["offset"] = offset
        resp = self._request(
            "GET",
            f"/api/v1/calibrations/{quote(project, safe='')}/traces",
            params=params or None,
        )
        self._check(resp)
        return resp.json()["traces"]

    # -- Judge Runs --

    def run_judge(
        self, project: str, traces: list[dict], rejudge: bool = False,
    ) -> dict:
        """Execute a full judge run on the backend.

        Args:
            project: Project name.
            traces: List of dicts with trace_id, input, output.
            rejudge: Re-score already-judged traces.

        Returns:
            JudgeRunExecuteResponse dict with per-trace results.
        """
        resp = self._request(
            "POST",
            f"/api/v1/judge-runs/{quote(project, safe='')}/run",
            json={"traces": traces, "rejudge": rejudge},
            timeout=300.0,
        )
        self._check(resp)
        return resp.json()

    def list_judge_runs(
        self,
        project: str,
        limit: int | None = None,
        offset: int = 0,
    ) -> list[dict]:
        """List judge runs for a project (most recent first)."""
        params: dict = {}
        if limit is not None:
            params["limit"] = limit
            params["offset"] = offset
        resp = self._request(
            "GET",
            f"/api/v1/judge-runs/{quote(project, safe='')}",
            params=params or None,
        )
        self._check(resp)
        return resp.json()["runs"]

    # -- Analyze --

    def analyze_basics(
        self,
        traces: list[dict],
        evaluations: dict[str, dict] | None = None,
    ) -> dict:
        """Run deterministic analysis on the backend.

        Args:
            traces: List of trace dicts with spans.
            evaluations: Optional dict of trace_id -> evaluation dict.

        Returns:
            Dict with stats, patterns, and samples.
        """
        payload: dict = {"traces": traces}
        if evaluations:
            payload["evaluations"] = evaluations
        resp = self._request(
            "POST", "/api/v1/analyze/basics", json=payload, timeout=60.0,
        )
        self._check(resp)
        return resp.json()

    # -- Assertion Runs --

    def write_assertion_run(self, project: str, result: dict) -> dict:
        """Store an assertion run result."""
        resp = self._request(
            "POST", f"/api/v1/assertion-runs/{quote(project, safe='')}", json=result
        )
        self._check(resp, expected=201)
        return resp.json()

    # -- Org Storage --

    def get_org_storage(self) -> dict | None:
        """Get org storage config. Returns None if not configured."""
        resp = self._request("GET", "/api/v1/storage")
        if resp.status_code == 404:
            return None
        self._check(resp)
        return resp.json()

    def save_org_storage(self, backend: str, config: dict) -> dict:
        """Set org storage config."""
        resp = self._request(
            "PUT", "/api/v1/storage",
            json={"storage_backend": backend, "storage_config": config},
        )
        self._check(resp)
        return resp.json()

    # -- Auth (no API key required) --

    @classmethod
    def signup(cls, email: str, password: str, base_url: str | None = None) -> dict:
        """Create account. Returns {"access_token": ...} or raises."""
        import httpx

        url = cls._resolve_base_url(base_url)
        try:
            resp = httpx.post(
                f"{url}/api/v1/auth/signup",
                data={"email": email, "password": password},
                timeout=30.0,
            )
        except (httpx.TimeoutException, httpx.ConnectError) as e:
            raise RuntimeError(f"Could not reach Bandito API: {e}")

        if resp.status_code == 400:
            detail = resp.json().get("detail", "")
            if "already" in detail.lower():
                return cls.login(email, password, base_url)
            raise RuntimeError(f"Signup failed: {detail}")
        if resp.status_code != 200:
            raise RuntimeError(f"Signup failed: {resp.text}")
        return resp.json()

    @classmethod
    def login(cls, email: str, password: str, base_url: str | None = None) -> dict:
        """Login with email/password. Returns {"access_token": ...} or raises."""
        import httpx

        url = cls._resolve_base_url(base_url)
        try:
            resp = httpx.post(
                f"{url}/api/v1/auth/login",
                data={"username": email, "password": password},
                timeout=30.0,
            )
        except (httpx.TimeoutException, httpx.ConnectError) as e:
            raise RuntimeError(f"Could not reach Bandito API: {e}")

        if resp.status_code != 200:
            detail = resp.json().get("detail", "wrong password")
            raise RuntimeError(f"Login failed: {detail}")
        return resp.json()

    @classmethod
    def create_api_key(cls, token: str, name: str = "cli", base_url: str | None = None) -> str:
        """Create API key using JWT token. Returns the raw key string."""
        import httpx

        url = cls._resolve_base_url(base_url)
        try:
            resp = httpx.post(
                f"{url}/api/v1/auth/api-keys",
                json={"name": name},
                headers={"Authorization": f"Bearer {token}"},
                timeout=30.0,
            )
        except (httpx.TimeoutException, httpx.ConnectError) as e:
            raise RuntimeError(f"Could not reach Bandito API: {e}")

        if resp.status_code not in (200, 201):
            raise RuntimeError(f"Could not create API key: {resp.text}")
        return resp.json()["raw_key"]

    @staticmethod
    def _resolve_base_url(base_url: str | None = None) -> str:
        if base_url:
            return base_url.rstrip("/")
        env = os.environ.get("BANDITO_BASE_URL")
        if env:
            return env.rstrip("/")
        return _DEFAULT_BASE_URL

    # -- Internal --

    def _request(self, method: str, path: str, timeout: float = 30.0, **kwargs) -> "httpx.Response":
        import httpx

        url = f"{self._base_url}{path}"
        try:
            return httpx.request(
                method,
                url,
                headers={"X-API-Key": self._api_key},
                timeout=timeout,
                **kwargs,
            )
        except httpx.TimeoutException:
            raise RuntimeError(
                "Bandito API request timed out. Try again."
            )
        except httpx.ConnectError:
            raise RuntimeError(
                f"Cannot connect to Bandito API at {self._base_url}. "
                "Check your network or BANDITO_BASE_URL setting."
            )

    def _check(self, resp: "httpx.Response", expected: int = 200) -> None:
        if resp.status_code == expected:
            return

        detail = ""
        try:
            detail = resp.json().get("detail", "")
        except Exception:
            detail = resp.text

        if resp.status_code == 401:
            raise RuntimeError(
                "Invalid BANDITO_API_KEY. "
                "Check your key or create one at https://bandito.dev/signup"
            )
        if resp.status_code == 402:
            raise RuntimeError(f"Plan limit reached. {detail}")
        if resp.status_code == 429:
            raise RuntimeError(
                "Rate limit exceeded. Try again in a moment."
            )
        raise RuntimeError(
            f"Bandito API error {resp.status_code}: {detail}"
        )

    # -- Summaries (Layer 2) --

    def sync_summaries(self, summaries: list[dict]) -> int:
        """Sync trace summaries to API.
        
        Args:
            summaries: List of summary dicts with trace_id, project, started_at,
                      ended_at, status, span_count, total_cost, latency, tags.
        
        Returns:
            Number of summaries synced.
        """
        resp = self._request(
            "POST",
            "/api/v1/summaries/sync",
            json={"summaries": summaries},
            timeout=30.0,
        )
        self._check(resp)
        return resp.json()["synced"]

    def list_summaries(
        self,
        project: str,
        limit: int = 100,
        offset: int = 0,
        since: str | None = None,
        tag: str | None = None,
    ) -> dict:
        """List trace summaries for a project.
        
        Args:
            project: Project name.
            limit: Max summaries to return.
            offset: Pagination offset.
            since: ISO timestamp filter.
            tag: Filter by tag.
        
        Returns:
            Dict with summaries and pagination info.
        """
        params: dict[str, Any] = {"limit": limit, "offset": offset}
        if since:
            params["since"] = since
        if tag:
            params["tag"] = tag
        resp = self._request(
            "GET",
            f"/api/v1/summaries/projects/{quote(project, safe='')}/summaries",
            params=params,
        )
        self._check(resp)
        return resp.json()

    def get_project_stats(self, project: str) -> dict:
        """Get aggregated stats for a project.
        
        Args:
            project: Project name.
        
        Returns:
            Dict with project stats.
        """
        resp = self._request(
            "GET",
            f"/api/v1/summaries/projects/{quote(project, safe='')}/stats",
        )
        self._check(resp)
        return resp.json()

    def list_projects(self) -> list[str]:
        """List all projects with summaries.
        
        Returns:
            List of project names.
        """
        resp = self._request("GET", "/api/v1/summaries/projects")
        self._check(resp)
        return resp.json()

    # -- Datasets --

    def create_dataset(
        self,
        name: str,
        project: str,
        description: str | None = None,
    ) -> dict:
        """Create a new dataset.
        
        Args:
            name: Dataset name.
            project: Project name.
            description: Optional description.
        
        Returns:
            Dataset dict.
        """
        resp = self._request(
            "POST",
            "/api/v1/datasets",
            json={"name": name, "project": project, "description": description},
        )
        self._check(resp, expected=201)
        return resp.json()

    def list_datasets(self, project: str) -> list[dict]:
        """List all datasets for a project.
        
        Args:
            project: Project name.
        
        Returns:
            List of dataset dicts.
        """
        resp = self._request(
            "GET",
            "/api/v1/datasets",
            params={"project": project},
        )
        self._check(resp)
        return resp.json()["datasets"]

    def get_dataset(self, dataset_id: int) -> dict:
        """Get a dataset with its items.
        
        Args:
            dataset_id: Dataset ID.
        
        Returns:
            Dataset dict with items.
        """
        resp = self._request("GET", f"/api/v1/datasets/{dataset_id}")
        self._check(resp)
        return resp.json()

    def delete_dataset(self, dataset_id: int) -> None:
        """Delete a dataset.
        
        Args:
            dataset_id: Dataset ID.
        """
        resp = self._request("DELETE", f"/api/v1/datasets/{dataset_id}")
        self._check(resp)

    def add_dataset_item(
        self,
        dataset_id: int,
        trace_id: str,
        source: str,
        metadata: dict | None = None,
    ) -> dict:
        """Add an item to a dataset.
        
        Args:
            dataset_id: Dataset ID.
            trace_id: Trace ID to add.
            source: Source of the item (human, judge, assertion, manual).
            metadata: Optional metadata dict.
        
        Returns:
            DatasetItem dict.
        """
        resp = self._request(
            "POST",
            f"/api/v1/datasets/{dataset_id}/items",
            json={"trace_id": trace_id, "source": source, "metadata": metadata},
        )
        self._check(resp, expected=201)
        return resp.json()

    def delete_dataset_item(self, dataset_id: int, item_id: int) -> None:
        """Remove an item from a dataset.
        
        Args:
            dataset_id: Dataset ID.
            item_id: Item ID to remove.
        """
        resp = self._request(
            "DELETE",
            f"/api/v1/datasets/{dataset_id}/items/{item_id}",
        )
        self._check(resp)
