"""
Cyphrex Python SDK — call ``check()`` before every agent action.
"""

from __future__ import annotations

import os
from typing import Any, Dict, Optional, Union

import requests

DEFAULT_API_URL = "https://cyphrexapi-production.up.railway.app"


def _normalize_response(data: Dict[str, Any]) -> Dict[str, Any]:
    allowed = data.get("allowed") is True
    out: Dict[str, Any] = {"allowed": allowed}
    if "reason" in data and data["reason"] is not None:
        out["reason"] = str(data["reason"])
    if "severity" in data and data["severity"] is not None:
        sev = str(data["severity"])
        out["severity"] = sev
        if not allowed:
            out["violations"] = [sev]
    if "spend" in data and isinstance(data["spend"], dict):
        out["spend"] = data["spend"]
    if "warning" in data:
        out["warning"] = data["warning"]
    return out


class Cyphrex:
    """
    Client for the Cyphrex check API.

    :param api_key: Account API key from Dashboard → API Keys
    :param agent_id: UUID of the agent
    :param api_url: API base URL (optional; default production)
    :param timeout: Request timeout in seconds (default 10)
    :param fail_open: If True, return allowed=True on network errors (default False)
    """

    def __init__(
        self,
        api_key: str,
        agent_id: str,
        api_url: Optional[str] = None,
        *,
        timeout: float = 10.0,
        fail_open: bool = False,
    ) -> None:
        if not (api_key or "").strip():
            raise ValueError("Cyphrex: api_key is required")
        if not (agent_id or "").strip():
            raise ValueError("Cyphrex: agent_id is required")
        self._api_key = api_key.strip()
        self._agent_id = agent_id.strip()
        self._api_url = (api_url or os.getenv("CYPHREX_API_URL") or DEFAULT_API_URL).rstrip("/")
        self._timeout = timeout
        self._fail_open = fail_open

    def check(
        self,
        action_type: str,
        url: Optional[str] = None,
        payload: Optional[Any] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """
        Ask Cyphrex whether an action is allowed.

        For ``data_access``-style checks, put ``dataScope`` in ``metadata`` (or use
        ``check_data_access()``). For LLM cost limits, put ``estimatedCost`` in
        ``metadata`` or use ``check_llm_input(..., estimated_cost=...)``.
        """
        return self._check_impl(action_type, url, payload, metadata)

    def _check_impl(
        self,
        action_type: str,
        url: Optional[str],
        payload: Optional[Any],
        metadata: Optional[Dict[str, Any]],
        *,
        data_scope: Optional[str] = None,
        estimated_cost: Optional[float] = None,
    ) -> Dict[str, Any]:
        if not (action_type or "").strip():
            return {
                "allowed": False,
                "reason": "action_type is required",
                "violations": ["validation"],
            }

        action: Dict[str, Any] = {"type": action_type.strip()}
        if url is not None:
            action["url"] = url
        if payload is not None:
            action["payload"] = payload

        meta = dict(metadata) if metadata else {}
        if data_scope is not None:
            action["dataScope"] = data_scope
        elif "dataScope" in meta:
            action["dataScope"] = meta.pop("dataScope")
        if estimated_cost is not None:
            action["estimatedCost"] = estimated_cost
        elif "estimatedCost" in meta:
            action["estimatedCost"] = meta.pop("estimatedCost")
        if meta:
            action["metadata"] = meta

        body = {"agentId": self._agent_id, "action": action}
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self._api_key}",
        }

        try:
            r = requests.post(
                f"{self._api_url}/v1/check",
                json=body,
                headers=headers,
                timeout=self._timeout,
            )
        except requests.RequestException as e:
            if self._fail_open:
                return {
                    "allowed": True,
                    "warning": f"Cyphrex unreachable ({e!s}) — allowed by fail_open",
                }
            return {
                "allowed": False,
                "reason": f"Request failed: {e!s}",
                "violations": ["network"],
            }

        try:
            data = r.json()
        except ValueError:
            return {
                "allowed": False,
                "reason": "Invalid JSON response from Cyphrex",
                "violations": ["parse_error"],
            }

        if r.status_code == 401:
            return {
                "allowed": False,
                "reason": str(data.get("error") or "Invalid API key"),
                "violations": ["unauthorized"],
            }
        if r.status_code == 400:
            return {
                "allowed": False,
                "reason": str(data.get("error") or "Bad request"),
                "violations": ["bad_request"],
            }
        if r.status_code == 404:
            return {
                "allowed": False,
                "reason": str(data.get("error") or "Agent not found or inactive"),
                "violations": ["not_found"],
            }
        if r.status_code >= 500:
            if self._fail_open:
                return {"allowed": True, "warning": "Cyphrex server error — allowed by fail_open"}
            return {
                "allowed": False,
                "reason": str(data.get("error") or "Cyphrex server error"),
                "violations": ["server_error"],
            }

        return _normalize_response(data)

    def check_http_call(
        self,
        url: str,
        *,
        payload: Optional[Any] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        return self._check_impl("http_call", url, payload, metadata)

    def check_data_access(
        self,
        data_scope: str,
        *,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        return self._check_impl("data_access", None, None, metadata, data_scope=data_scope)

    def check_llm_input(
        self,
        prompt: Union[str, Dict[str, Any]],
        *,
        estimated_cost: Optional[float] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        pl: Any = prompt[:2048] if isinstance(prompt, str) else prompt
        return self._check_impl(
            "llm_input",
            None,
            pl,
            metadata,
            estimated_cost=estimated_cost,
        )
