"""Worldtree Conversation API caller-semantic types + exceptions.

Post-#20 cutover this module holds NO client: the worldtree-sdk adapter
(`ratatoskr.wt`) owns the session/agents wire. What remains is ratatoskr's
caller-semantic exception surface (raised by `wt`, caught by the CLI/web
presenters), the `BifrostBinding` dataclass, and the `endpoint_for_plane`
provider helper. The former issue #2 wire contract is retired (cutover DEC-1);
the Tier-3 exceptions are homed here (not in `tier3`) for the class-identity
reason noted below.
"""

from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True)
class BifrostBinding:
    """Session-create Bifrost binding (Worldtree BifrostBindingRequest, #160).

    Issue #17. `endpoint_url` is the WORLDTREE-VISIBLE base URL of ONE provider
    plane (memory :8391 / affect :8390). `scope` is an opaque pass-through copied
    into the handshake JWT unchanged (≤256 chars); ratatoskr does not interpret
    it and v1 sends None.
    """

    endpoint_url: str
    scope: str | None = None


class AgentNotFound(Exception):
    """Raised on HTTP 404 from POST /sessions — unknown agent_id."""

    def __init__(self, *, agent_id: str) -> None:
        super().__init__(f"unknown agent_id: {agent_id!r}")
        self.agent_id = agent_id


class InvalidCursor(Exception):
    """Raised on HTTP 422 cursor_invalid from GET /sessions."""

    def __init__(self, *, raw: str | None) -> None:
        super().__init__(f"server rejected cursor: {raw!r}")
        self.raw = raw


class SessionApiFailed(Exception):
    """Raised on unexpected response status from /sessions endpoints.

    `body` is truncated to ≤ 1024 bytes at construction (INV-004), consistent
    with the issue #1 precedent for `SseConnectFailed` / `CancelFailed`.
    """

    def __init__(self, *, status: int, body: bytes) -> None:
        body = body[:1024]
        super().__init__(f"sessions API failed: status={status}, body={body[:128]!r}")
        self.status = status
        self.body = body


# Issue #17 — Bifrost-bind failure modes on POST /sessions.
class BifrostConsumerKeyMissing(Exception):
    """A bifrost binding was requested without a consumer_key.

    Raised BEFORE any HTTP (INV-001): the bound create must never silently fall
    back to the client's default canary key — the consumer key IS the handshake
    identity Worldtree signs the Bifrost JWT with.
    """

    def __init__(self) -> None:
        super().__init__(
            "bifrost binding requires a non-empty consumer_key; refusing to "
            "fall back to the canary key (INV-001)"
        )


class BifrostHandshakeFailed(Exception):
    """Raised on HTTP 502 `bifrost_handshake_failed` from a bound POST /sessions.

    INV-002: the Bifrost handshake runs synchronously at session-create, so a
    handshake failure (bad URL / down provider / wrong key / HTTPS rejection)
    fails SESSION CREATION — surfaced on the create path, never deferred to the
    first turn. `bifrost_error` carries the spec-level code (e.g.
    `bifrost.auth_rejected`); `body` is truncated to ≤1024 bytes, consistent with
    `SessionApiFailed` (INV-004 precedent).
    """

    def __init__(self, *, bifrost_error: str | None, body: bytes) -> None:
        body = body[:1024]
        super().__init__(f"bifrost handshake failed: bifrost_error={bifrost_error!r}")
        self.bifrost_error = bifrost_error
        self.body = body


# Worldtree #204 / v0.28.0 — persona_state endpoint failure modes.
class PersonaNotConfigured(Exception):
    """Raised on HTTP 404 `persona_not_configured` from GET persona_state.

    Agent exists but has no persona surface: persona-disabled Tier 1/2
    agents (e.g. `domari`, `muninn`) and all Tier 3 consumer-defined
    agents (Phase 2.0). Distinct from `AgentNotAvailable` which means the
    agent_id is unknown entirely.
    """

    def __init__(self, *, agent_id: str) -> None:
        super().__init__(f"persona not configured for agent_id: {agent_id!r}")
        self.agent_id = agent_id


class AgentNotAvailable(Exception):
    """Raised on HTTP 404 `agent_not_available` from GET persona_state.

    The agent_id is unknown to the server. Distinct from
    `PersonaNotConfigured` (agent exists but has no persona).
    """

    def __init__(self, *, agent_id: str) -> None:
        super().__init__(f"agent not available: {agent_id!r}")
        self.agent_id = agent_id


class AuthScopeDenied(Exception):
    """Raised on HTTP 403 `auth_scope_denied` from a Heimdall-scoped endpoint.

    The API key lacks the required scope (e.g. `persona.read` for
    GET /agents/{id}/persona_state). User-tier keys carry `persona.read`
    by default; this surfaces when a narrower key is in use.
    """

    def __init__(self, *, scope: str) -> None:
        super().__init__(f"auth scope denied: required={scope!r}")
        self.scope = scope


class AuthoredHistoryUnavailable(Exception):
    """Raised on HTTP 404 from POST /sessions/{id}/history (#347 authored-history-write).

    Hide-existence (server INV-347-1): an ungranted caller, a non-owner, and an
    unknown session ALL receive a 404 byte-identical to a genuine
    `session_not_found` — the feature's existence is never revealed by status,
    body, or error_code. The consumer MUST treat this as feature-absent and fall
    back (a production consumer to a model-generated greeting), and MUST NOT
    capability-probe to distinguish the causes. Distinct from `SessionApiFailed`
    so callers branch feature-absent without inspecting a status code.
    """

    def __init__(self, *, session_id: str) -> None:
        super().__init__(
            f"authored-history write unavailable for session {session_id!r} "
            "(404 hide-existence: feature-absent / ungranted / session-absent)"
        )
        self.session_id = session_id


# ── Tier-3 (consumer-defined) agent lifecycle exceptions ─────────────────────
# The worldtree-sdk adapter (`ratatoskr.wt`) re-raises these from the agents.define/
# patch/delete routes. They live HERE (not in `tier3`) so both `wt` and the
# `python -m ratatoskr.tier3` CLI reference the SAME class objects: `tier3` is run as
# `__main__`, and if these were defined there, `wt`'s `from .tier3 import …` would bind
# a SECOND copy under `ratatoskr.tier3` — so a raised exception would not match the
# CLI's `except` (the exception would escape as an uncaught traceback). Homing them in
# `sessions` (never run as `__main__`) makes the class identity single.


class Tier3QuotaExceeded(Exception):
    """Raised on HTTP 429 ``agent_quota_exceeded`` — 50-agent cap reached
    on the Heimdall key. ``retry_after`` captures the Retry-After header
    verbatim (defaults to 0 per spec §2675; forward-compat for non-zero).

    Post-cutover the adapter constructs this with ``retry_after=0`` — the SDK's
    ``ApiError`` floor carries no response headers, and §2675 pins Phase-2.0 quota
    to ``Retry-After: 0``, so the value is spec-canonical."""

    def __init__(self, *, retry_after: int) -> None:
        super().__init__(f"Tier 3 agent quota exceeded (retry_after={retry_after})")
        self.retry_after = retry_after


class Tier3UserIdUnsupported(Exception):
    """Raised on HTTP 403 ``tier3_user_id_unsupported`` — auth's user_id
    is not slug-safe per Phase 2.0 gate (spec §2626)."""

    def __init__(self) -> None:
        super().__init__("tier3 caller user_id is not slug-safe")


class Tier3FieldNotMutable(Exception):
    """Raised on HTTP 422 ``field_not_mutable`` — PATCH request body
    carried a key that's immutable post-define (``agent_name``, ``user_id``,
    or any layer field). Server rejects BEFORE the DB lookup (spec §2644)."""

    def __init__(self, *, field: str | None) -> None:
        super().__init__(f"field not mutable on Tier 3 patch: {field!r}")
        self.field = field


class Tier3LayerDeferred(Exception):
    """Raised on HTTP 422 ``layer_deferred`` — define request carried a
    non-null layer field (``persona`` / ``motivational`` / ``valence`` /
    ``memory``). Phase 2.0 ships baseline only; layers are schema-reserved.

    Note: ``define_agent`` never sends layer fields, so this exception is
    defense-against-server-side-changes / forward-compat."""

    def __init__(self, *, field: str | None) -> None:
        super().__init__(f"tier3 layer field deferred: {field!r}")
        self.field = field


class Tier3AgentNotFound(Exception):
    """Raised on HTTP 404 — PATCH or DELETE on a non-existent agent_id
    (spec §2634 + §2641)."""

    def __init__(self, *, agent_id: str) -> None:
        super().__init__(f"tier3 agent not found: {agent_id!r}")
        self.agent_id = agent_id


def endpoint_for_plane(plane: str, base_host: str) -> str:
    """Map a provider plane name to its Worldtree-VISIBLE base URL.

    Issue #17 dev helper: `memory` → :8391, `affect` → :8390, `combined` → :8392
    (the #18 composite both-plane endpoint). Returns the Worldtree-visible base
    (e.g. `http://10.100.10.50:8391`), NOT the client's loopback — Worldtree must
    reach the provider over the network. `http://` is deliberate: the HTTPS
    relaxation is allowlist-side (Worldtree's BIFROST_CLIENT_ALLOWED_HOSTS), not a
    URL concern. A production HTTPS endpoint is supplied directly, bypassing this
    helper.
    """
    ports = {"memory": 8391, "affect": 8390, "combined": 8392}
    if plane not in ports:
        raise ValueError(f"unknown plane: {plane!r} (expected 'memory', 'affect', or 'combined')")
    return f"http://{base_host}:{ports[plane]}"
