"""Python SDK for the `be` (Hungarian Legal AI) HTTP API.

Single-file, dependency-light — requires only `httpx`. Works against any
`be` deployment by passing the base URL + API key.

Usage:

    from jurisafe_sdk import JuriSafeClient

    js = JuriSafeClient("https://app.jurisafeai.com", api_key="jsafe_apikey_…")

    # One-shot question (collects the streamed answer into one string)
    answer = js.ask("Mi a felmondási idő munkáltatói felmondásnál?")
    print(answer)

    # Streaming (yields AgentEvent dicts as they arrive)
    for ev in js.chat_stream(
        messages=[{"role": "user", "content": "Készíts egy fizetési felszólítást."}],
        preset="vallalkozoi",
    ):
        if ev["type"] in ("text", "final") and ev.get("text"):
            print(ev["text"], end="", flush=True)

    # Resource helpers
    convs = be.conversations.list()
    be.deadlines.create(due_date="2026-06-15", label="Fellebbezés")
    statute = js.statute(law="Ptk.", section="6:8. §")

The client only knows how to talk to `be`'s documented endpoints; it does
not depend on the server's internal modules. Designed for portability.
"""

from __future__ import annotations

import json
from collections.abc import Iterator
from dataclasses import dataclass
from typing import Any

try:
    import httpx
except ImportError as e:  # pragma: no cover
    raise ImportError(
        "jurisafe_sdk requires httpx. Install with: pip install httpx"
    ) from e


__all__ = ["JuriSafeClient", "BeError"]


class BeError(RuntimeError):
    """Raised on non-2xx HTTP responses from the `be` API."""

    def __init__(self, status: int, body: Any):
        self.status = status
        self.body = body
        msg = body.get("detail") if isinstance(body, dict) else body
        super().__init__(f"[{status}] {msg}")


# --------------------------------------------------------------------------- #
# Resource adapters
# --------------------------------------------------------------------------- #
@dataclass
class _Resource:
    client: JuriSafeClient
    path: str

    def list(self, **params: Any) -> list[dict]:
        return self.client._request("GET", self.path, params=params)

    def get(self, item_id: str) -> dict:
        return self.client._request("GET", f"{self.path}/{item_id}")

    def delete(self, item_id: str) -> dict:
        return self.client._request("DELETE", f"{self.path}/{item_id}")


class _Conversations(_Resource):
    def export_pdf(self, conv_id: str) -> bytes:
        r = self.client._raw("GET", f"{self.path}/{conv_id}/export.pdf")
        return r.content

    def export_docx(self, conv_id: str) -> bytes:
        r = self.client._raw("GET", f"{self.path}/{conv_id}/export.docx")
        return r.content

    def rename(self, conv_id: str, title: str) -> dict:
        return self.client._request("PATCH", f"{self.path}/{conv_id}/title", json={"title": title})

    def pin(self, conv_id: str, pinned: bool = True) -> dict:
        return self.client._request("PATCH", f"{self.path}/{conv_id}/pin", json={"pinned": pinned})

    def restore(self, conv_id: str) -> dict:
        return self.client._request("POST", f"{self.path}/{conv_id}/restore")

    def trashed(self) -> list[dict]:
        return self.client._request("GET", f"{self.path}/trashed")


class _Deadlines(_Resource):
    def create(self, due_date: str, label: str, conv_id: str | None = None) -> dict:
        return self.client._request(
            "POST", self.path,
            json={"due_date": due_date, "label": label, "conv_id": conv_id},
        )

    def ics(self) -> str:
        return self.client._raw("GET", f"{self.path}.ics").text


class _Clients(_Resource):
    def create(self, client_id: str, name: str) -> dict:
        return self.client._request(
            "POST", self.path, json={"id": client_id, "name": name},
        )


class _Templates(_Resource):
    def create(
        self, key: str, title: str, description: str, markdown: str,
    ) -> dict:
        return self.client._request(
            "POST", self.path,
            json={
                "key": key, "title": title,
                "description": description, "markdown": markdown,
            },
        )


class _Documents(_Resource):
    """Generated documents — list, read content, and TRANSMIT (download bytes).

    Documents are produced by the agent (see ``JuriSafeClient.generate_document``);
    these methods let a caller pull them out in any format and ship them
    onward (e-mail, Word, Google Docs, DMS…)."""

    def list(self) -> list[dict]:
        return self.client._request("GET", self.path)

    def get(self, doc_id: str) -> dict:
        """Full record incl. markdown + which formats exist on disk."""
        return self.client._request("GET", f"{self.path}/{doc_id}")

    def download(self, doc_id: str, fmt: str = "pdf") -> bytes:
        """Raw bytes of a rendering (``md`` | ``html`` | ``pdf`` | ``docx``) —
        ready to attach/transmit anywhere."""
        return self.client._raw("GET", f"{self.path}/{doc_id}/{fmt}").content

    def save(self, doc_id: str, markdown: str) -> dict:
        return self.client._request(
            "POST", f"{self.path}/save", json={"doc_id": doc_id, "markdown": markdown}
        )


# --------------------------------------------------------------------------- #
# Main client
# --------------------------------------------------------------------------- #
class JuriSafeClient:
    """Synchronous HTTP client for the `be` Hungarian legal AI API.

    Args:
        base_url: deployment root, e.g. ``https://app.jurisafeai.com`` (no trailing slash).
        api_key:  personal API key (``jsafe_apikey_…``). Optional — anonymous
            calls share the freemium tier (3 messages/day default).
        x402_token: optional ``x402_…`` payment token for paid agent calls
            when the server has x402 enabled.
        timeout: per-request timeout in seconds.
    """

    USER_AGENT = "jurisafe-sdk-python/1.0"

    def __init__(
        self,
        base_url: str,
        *,
        api_key: str | None = None,
        x402_token: str | None = None,
        timeout: float = 60.0,
    ) -> None:
        self.base_url = base_url.rstrip("/")
        self.api_key = api_key
        self.x402_token = x402_token
        self._http = httpx.Client(timeout=timeout, follow_redirects=True)
        # Resource adapters bound to this client.
        self.conversations = _Conversations(self, "/api/conversations")
        self.deadlines = _Deadlines(self, "/api/deadlines")
        self.clients = _Clients(self, "/api/clients")
        self.templates = _Templates(self, "/api/templates")
        self.documents = _Documents(self, "/api/documents")

    # -------- core HTTP --------
    def _headers(self, extra: dict | None = None) -> dict[str, str]:
        h: dict[str, str] = {"User-Agent": self.USER_AGENT}
        if self.api_key:
            h["Authorization"] = f"Bearer {self.api_key}"
        elif self.x402_token:
            h["X-PAYMENT"] = self.x402_token
        # Same-origin header so the CSRF guard accepts the request even
        # when the server is configured strictly.
        h["Origin"] = self.base_url
        if extra:
            h.update(extra)
        return h

    def _raw(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
        headers = self._headers(kwargs.pop("headers", None))
        r = self._http.request(method, self.base_url + path, headers=headers, **kwargs)
        if r.status_code >= 400:
            try:
                body = r.json()
            except Exception:
                body = r.text
            raise BeError(r.status_code, body)
        return r

    def _request(self, method: str, path: str, **kwargs: Any) -> Any:
        return self._raw(method, path, **kwargs).json()

    # -------- chat --------
    def chat_stream(
        self,
        messages: list[dict[str, str]],
        *,
        conversation_id: str | None = None,
        preset: str | None = None,
        provider: str | None = None,
    ) -> Iterator[dict[str, Any]]:
        """Stream the SSE chat response as ``AgentEvent`` dicts.

        Each yielded event has shape::

            {"type": "text"|"tool_call"|"tool_result"|"final"|"sources"|"error",
             "text": "...", "tool_name": None|"...", "data": {...}}
        """
        payload = {
            "messages": messages,
            "conversation_id": conversation_id,
            "preset": preset,
            "provider": provider,
        }
        headers = self._headers({"Accept": "text/event-stream"})
        with self._http.stream(
            "POST", self.base_url + "/api/chat",
            json=payload, headers=headers,
        ) as r:
            if r.status_code >= 400:
                body: Any
                try:
                    body = r.read().decode()
                    body = json.loads(body)
                except Exception:
                    pass
                raise BeError(r.status_code, body)
            for line in r.iter_lines():
                if not line or not line.startswith("data: "):
                    continue
                data = line[6:]
                if data == "[DONE]":
                    return
                try:
                    yield json.loads(data)
                except json.JSONDecodeError:
                    continue

    def ask(
        self,
        question: str,
        *,
        conversation_id: str | None = None,
        preset: str | None = None,
    ) -> str:
        """One-shot helper: collect the streamed answer into a single string."""
        parts: list[str] = []
        for ev in self.chat_stream(
            messages=[{"role": "user", "content": question}],
            conversation_id=conversation_id,
            preset=preset,
        ):
            if ev.get("type") in ("text", "final") and ev.get("text"):
                parts.append(ev["text"])
        # Streaming sends incremental deltas already concatenated server-side;
        # take the longest fragment as the final canonical text.
        return max(parts, key=len) if parts else ""

    # -------- catalogue / metadata --------
    def health(self) -> dict:
        return self._request("GET", "/api/health")

    def presets(self) -> list[dict]:
        return self._request("GET", "/api/presets")

    def statute(self, law: str, section: str) -> dict:
        """Fetch a precise statute text by short reference + section."""
        return self._request(
            "GET", "/api/statute", params={"law": law, "section": section},
        )

    def search(self, q: str, *, limit: int = 20, kind: str | None = None) -> list[dict]:
        return self._request(
            "GET", "/api/search",
            params={"q": q, "limit": limit, "kind": kind} if kind else
                   {"q": q, "limit": limit},
        )

    def generate_document(self, instructions: str, *, preset: str | None = None) -> dict | None:
        """Have the agent author a document from natural-language instructions,
        then return the newest document record. Download any format with
        ``client.documents.download(rec["doc_id"], "pdf")``."""
        before = {d.get("doc_id") for d in self.documents.list()}
        self.ask(instructions, preset=preset)
        docs = self.documents.list()
        fresh = [d for d in docs if d.get("doc_id") not in before]
        return (fresh or docs or [None])[0]

    # -------- storage mode (central <-> blockchain/decentralized) --------
    def storage_mode(self) -> dict:
        return self._request("GET", "/api/storage-mode")

    def set_storage_mode(self, mode: str) -> dict:
        """mode: ``"central"`` or ``"blockchain"`` (if enabled on the deployment)."""
        return self._request("POST", "/api/storage-mode", json={"mode": mode})

    # -------- auth helpers (optional) --------
    def me(self) -> dict:
        return self._request("GET", "/api/auth/me")

    def export_account(self) -> bytes:
        return self._raw("GET", "/api/auth/me/export").content

    def close(self) -> None:
        self._http.close()

    def __enter__(self) -> JuriSafeClient:
        return self

    def __exit__(self, *_: Any) -> None:
        self.close()
