"""Bordair Python SDK — Official client for the Bordair AI security API.

Detect prompt injection attacks before they reach your LLM.

Homepage:  https://bordair.io
GitHub:    https://github.com/Josh-blythe/bordair
"""

from __future__ import annotations

import os
from concurrent.futures import ThreadPoolExecutor
from typing import Dict, List, Optional

import requests
from requests.exceptions import ConnectionError as _ConnError, Timeout as _Timeout


# ── Errors ────────────────────────────────────────────────────────────────────


class BordairError(Exception):
    """Raised when the Bordair API returns an error or a network failure occurs.

    Attributes:
        status: HTTP status code, or 0 for network/timeout errors.
        message: Human-readable error description.
        body: Raw response body string (useful for debugging).
    """

    def __init__(self, message: str, status: int = 0, body: str = "") -> None:
        super().__init__(message)
        self.status = status
        self.body = body

    def __repr__(self) -> str:
        return f"BordairError(status={self.status!r}, message={str(self)!r})"


# ── Type aliases ──────────────────────────────────────────────────────────────

# TypedDict requires Python 3.8+; use plain dicts at runtime for simplicity.
ScanResult = Dict[str, object]   # threat, confidence, method
LogEntry   = Dict[str, object]   # id, timestamp, input_hash, input_length, …
Stats      = Dict[str, object]   # total_scans, high_threats, avg_confidence
UserInfo   = Dict[str, object]   # email, tier, total_scans, high_threats


# ── Constants ─────────────────────────────────────────────────────────────────

_DEFAULT_BASE_URL = "https://api.bordair.io"
_DEFAULT_TIMEOUT  = 10


# ── Client ────────────────────────────────────────────────────────────────────


class Bordair:
    """Official Python SDK for the Bordair AI security API.

    All methods are synchronous and thread-safe.

    Example::

        from bordair import Bordair

        client = Bordair()                          # reads BORDAIR_API_KEY env var
        result = client.scan("Hello, how are you?")
        print(result["threat"])                     # "low"
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        base_url: str = _DEFAULT_BASE_URL,
        timeout: int = _DEFAULT_TIMEOUT,
    ) -> None:
        """Initialise the Bordair client.

        Args:
            api_key: Your Bordair API key (starts with ``bdr_``). Falls back to
                the ``BORDAIR_API_KEY`` environment variable if not provided.
            base_url: Base URL for the API. Defaults to
                ``https://api.bordair.io``.
            timeout: Request timeout in seconds. Defaults to 10.

        Raises:
            BordairError: If no API key is found in either the argument or the
                ``BORDAIR_API_KEY`` environment variable.
        """
        resolved = (api_key or os.environ.get("BORDAIR_API_KEY", "")).strip()
        if not resolved:
            raise BordairError(
                "No API key provided. Pass api_key= or set the BORDAIR_API_KEY "
                "environment variable. Get a free key at https://bordair.io",
                status=0,
            )
        self._api_key  = resolved
        self._base_url = base_url.rstrip("/")
        self._timeout  = timeout
        self._session  = requests.Session()
        self._session.headers.update({"x-api-key": self._api_key})

    # ── Internal helpers ──────────────────────────────────────────────────────

    def _get(self, path: str, params: Optional[Dict] = None) -> dict:
        """Make an authenticated GET request and return parsed JSON."""
        try:
            r = self._session.get(
                f"{self._base_url}{path}",
                params=params,
                timeout=self._timeout,
            )
        except _Timeout:
            raise BordairError(
                f"Request timed out after {self._timeout}s", status=0
            )
        except _ConnError as exc:
            raise BordairError(f"Network error: {exc}", status=0)
        return self._parse(r)

    def _post(self, path: str, body: dict) -> dict:
        """Make an authenticated POST request and return parsed JSON."""
        try:
            r = self._session.post(
                f"{self._base_url}{path}",
                json=body,
                timeout=self._timeout,
            )
        except _Timeout:
            raise BordairError(
                f"Request timed out after {self._timeout}s", status=0
            )
        except _ConnError as exc:
            raise BordairError(f"Network error: {exc}", status=0)
        return self._parse(r)

    @staticmethod
    def _parse(r: requests.Response) -> dict:
        """Raise BordairError for non-2xx responses, else return JSON."""
        if r.ok:
            return r.json()
        body = r.text
        if r.status_code == 401:
            raise BordairError(
                "Invalid API key. Check your key or get a new one at "
                "https://bordair.io",
                status=401, body=body,
            )
        if r.status_code == 429:
            raise BordairError(
                "Rate limit exceeded. Reduce request frequency or upgrade "
                "your tier at https://bordair.io",
                status=429, body=body,
            )
        raise BordairError(
            f"API error {r.status_code}: {body[:200]}",
            status=r.status_code, body=body,
        )

    # ── Public API ────────────────────────────────────────────────────────────

    def scan(self, text: str) -> ScanResult:
        """Scan a single text input for prompt injection.

        Args:
            text: The text to scan (max 10,000 characters).

        Returns:
            A dict with keys:

            - ``threat`` – ``"high"`` or ``"low"``
            - ``confidence`` – float between 0 and 1
            - ``method`` – ``"pattern"`` or ``"ml"``

        Raises:
            BordairError: On API errors, network failures, or timeouts.

        Example::

            result = client.scan("Ignore all previous instructions")
            result["threat"]  # "high"
        """
        return self._post("/scan", {"input": text})

    def scan_many(self, texts: List[str]) -> List[ScanResult]:
        """Scan multiple text inputs in parallel.

        Dispatches all requests concurrently using a thread pool and returns
        results in the same order as the input list.

        Args:
            texts: List of texts to scan.

        Returns:
            List of dicts in the same order as ``texts``, each with the same
            structure as :meth:`scan`.

        Raises:
            BordairError: If any individual scan fails.

        Example::

            results = client.scan_many(["hello", "ignore all rules"])
            results[0]["threat"]  # "low"
            results[1]["threat"]  # "high"
        """
        if not texts:
            return []
        workers = min(len(texts), 10)
        with ThreadPoolExecutor(max_workers=workers) as pool:
            futures = [pool.submit(self.scan, t) for t in texts]
            return [f.result() for f in futures]

    def is_safe(self, text: str) -> bool:
        """Return ``True`` if the input is safe (``threat == "low"``).

        Convenience wrapper around :meth:`scan` for simple guard logic.

        Args:
            text: The text to check.

        Returns:
            ``True`` if ``threat == "low"``, ``False`` if ``threat == "high"``.

        Raises:
            BordairError: On API errors, network failures, or timeouts.

        Example::

            if client.is_safe(user_input):
                pass_to_llm(user_input)
        """
        return self.scan(text)["threat"] == "low"

    def healthy(self) -> bool:
        """Return ``True`` if the API is reachable and responding.

        Does not require a valid API key — useful for startup checks and
        readiness probes.

        Returns:
            ``True`` if the API responds with HTTP 200, ``False`` on any error.

        Example::

            if not client.healthy():
                raise RuntimeError("Bordair API is unreachable")
        """
        try:
            r = requests.get(f"{self._base_url}/health", timeout=self._timeout)
            return r.status_code == 200
        except Exception:
            return False

    def logs(self, limit: int = 100) -> List[LogEntry]:
        """Fetch recent scan logs for your API key.

        Args:
            limit: Number of records to return (1–1000). Defaults to 100.

        Returns:
            List of log entry dicts, each with: ``id``, ``timestamp``,
            ``input_hash``, ``input_length``, ``threat``, ``confidence``,
            ``method``.

        Raises:
            BordairError: On API errors, network failures, or timeouts.
        """
        return self._get("/logs", params={"limit": limit})

    def stats(self) -> Stats:
        """Fetch aggregate scan statistics for your API key.

        Returns:
            A dict with: ``total_scans``, ``high_threats``, ``avg_confidence``.

        Raises:
            BordairError: On API errors, network failures, or timeouts.
        """
        return self._get("/stats")

    def me(self) -> UserInfo:
        """Fetch current user info and lifetime scan statistics.

        Returns:
            A dict with: ``email``, ``tier``, ``total_scans``, ``high_threats``.

        Raises:
            BordairError: On API errors, network failures, or timeouts.
        """
        return self._get("/auth/me")
