"""Worldtree Tier 3 (consumer-defined) agent CLI + caller-semantic exceptions.

The define / patch / delete wire calls route through the worldtree-sdk adapter
(``ratatoskr.wt.define_agent`` / ``patch_agent`` / ``delete_agent``); this module owns
the ``python -m ratatoskr.tier3`` CLI and the Tier-3 caller-semantic exception
taxonomy the adapter re-raises (quota / user-id / field-not-mutable / layer-deferred /
not-found). The picker already handles colon-containing agent_ids generically
(issue #8).

The old hand-rolled httpx wrappers + the ``Tier3AgentInfo`` dataclass were deleted in
the worldtree-sdk cutover (issue #20, slice-4); the adapter returns the SDK's
open-world define/patch dicts (echoing ``role`` post-b128, spec 1.2), read here as
mappings. ``wt`` imports this module's exceptions at module level; this module imports
``wt`` only lazily inside the CLI handlers, so there is no import cycle.
"""

from __future__ import annotations

import argparse
import sys
from collections.abc import Mapping
from typing import Any

import httpx

# The Tier-3 caller-semantic exceptions live in `sessions` (never run as `__main__`)
# so the adapter's raise and this CLI's `except` reference the SAME class objects —
# see the header note in `sessions.py`. `main()` catches these; `wt` raises them.
from ratatoskr.sessions import (
    Tier3AgentNotFound,
    Tier3FieldNotMutable,
    Tier3LayerDeferred,
    Tier3QuotaExceeded,
    Tier3UserIdUnsupported,
)


def _str_field(info: Mapping[str, Any], key: str, *, default: str = "") -> str:
    """A string field off an open-world response dict, or `default` when the key is
    absent / null / non-string — so a partial or drifted 2xx define/patch response
    degrades rather than KeyError/AttributeError-crashing the CLI presenter (the
    "open-world reads degrade, never crash the presenter" invariant; heid-bug-hunt)."""
    value = info.get(key)
    return value if isinstance(value, str) else default

# ---- CLI (`python -m ratatoskr.tier3 <subcommand>`) ------------------------
#
# Auth + server URL resolution mirrors ratatoskr.cli verbatim. Exit codes
# mirror ratatoskr.cli: 0 happy / 10 usage / 11 auth / 20 api-failure /
# 21 network. Outbound requests carry the same User-Agent string. The wire calls
# route through the worldtree-sdk adapter (ratatoskr.wt) over a ratatoskr-owned
# injected transport (INV-CUT-1: the SDK never closes it).


class _Tier3UsageError(Exception):
    """Argparse usage violation → exit 10."""


class _Tier3AuthError(Exception):
    """No API key resolvable → exit 11."""


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="python -m ratatoskr.tier3",
        description="Worldtree Tier 3 (consumer-defined) agent lifecycle.",
    )
    parser.add_argument("--api-key", dest="api_key", default=None)
    parser.add_argument("--server", dest="server", default=None)
    sub = parser.add_subparsers(dest="cmd", required=True)

    p_define = sub.add_parser("define", help="Create a Tier 3 agent.")
    p_define.add_argument("--name", required=True, help="agent_name (slug).")
    p_define.add_argument(
        "--system-prompt", dest="system_prompt", required=True,
        help="System prompt the agent ships with.",
    )
    p_define.add_argument(
        "--role", required=True,
        help="Model role, e.g. thoughtful-character (see GET /models/available-for-characters).",
    )

    p_patch = sub.add_parser("patch", help="Mutate system_prompt and/or role.")
    p_patch.add_argument("agent_id", help='Full "<user_id>:<agent_name>" form.')
    p_patch.add_argument("--system-prompt", dest="system_prompt", default=None)
    p_patch.add_argument("--role", default=None)

    p_delete = sub.add_parser("delete", help="Hard-delete a Tier 3 agent.")
    p_delete.add_argument("agent_id", help='Full "<user_id>:<agent_name>" form.')

    return parser


def _resolve_auth(ns: argparse.Namespace) -> tuple[str, str]:
    """Resolve API key + server URL with the same env-var fallback as cli.py."""
    import os

    api_key = ns.api_key or os.environ.get("WORLDTREE_API_KEY") or ""
    if not api_key:
        raise _Tier3AuthError("no API key (set --api-key or WORLDTREE_API_KEY)")
    server_url = (
        ns.server or os.environ.get("WORLDTREE_API_URL") or "http://localhost:8000"
    )
    return api_key, server_url


def _transport(server_url: str, api_key: str) -> httpx.AsyncClient:
    """The ratatoskr-owned httpx transport the adapter's WorldtreeClient is built
    over. Carries the User-Agent + a generous read timeout for agent CRUD; the SDK
    re-applies auth per request (the default bearer here just mirrors it)."""
    from ratatoskr.cli import USER_AGENT

    return httpx.AsyncClient(
        base_url=server_url,
        headers={"Authorization": f"Bearer {api_key}", "User-Agent": USER_AGENT},
        timeout=httpx.Timeout(connect=10.0, read=30.0, write=10.0, pool=10.0),
    )


async def _run_define(ns: argparse.Namespace) -> int:
    api_key, server_url = _resolve_auth(ns)
    from ratatoskr import wt
    from ratatoskr.local_agents import (
        LocalAgentEntry,
        add_local_agent,
        make_description,
    )

    async with _transport(server_url, api_key) as transport:
        client = wt.build_client(server_url, api_key=api_key, transport=transport)
        info = await wt.define_agent(
            client,
            agent_name=ns.name,
            system_prompt=ns.system_prompt,
            role=ns.role,
        )
        # Open-world define dict — read defensively (echoes `role` post-b128, not
        # `model`). A partial/drifted 2xx must not crash the presenter.
        agent_id = _str_field(info, "agent_id")
        if not agent_id:  # a 2xx with no usable agent_id is a malformed success
            sys.stderr.write(f"[api_failed] define returned no usable agent_id: {info!r}\n")
            return 20
        role = _str_field(info, "role", default="?")
        agent_name = _str_field(info, "agent_name")
        # v0.8.0: persist to local index so the picker can show it — only for a
        # well-formed identity (agent_id + agent_name); else skip the write, still print.
        if agent_name:
            add_local_agent(
                LocalAgentEntry(
                    agent_id=agent_id,
                    agent_name=agent_name,
                    role=role,
                    description=make_description(_str_field(info, "system_prompt")),
                    defined_at=_str_field(info, "created_at"),
                )
            )
        print(f"defined {agent_id} ({role})")
        return 0


async def _run_patch(ns: argparse.Namespace) -> int:
    api_key, server_url = _resolve_auth(ns)
    from ratatoskr import wt
    from ratatoskr.local_agents import (
        LocalAgentEntry,
        make_description,
        update_local_agent,
    )

    if ns.system_prompt is None and ns.role is None:
        raise _Tier3UsageError(
            "patch requires at least one of --system-prompt or --role"
        )
    async with _transport(server_url, api_key) as transport:
        client = wt.build_client(server_url, api_key=api_key, transport=transport)
        info = await wt.patch_agent(
            client,
            ns.agent_id,
            system_prompt=ns.system_prompt,
            role=ns.role,
        )
        # Open-world patch dict — read defensively (same degrade-not-crash posture).
        agent_id = _str_field(info, "agent_id")
        if not agent_id:  # a 2xx with no usable agent_id is a malformed success
            sys.stderr.write(f"[api_failed] patch returned no usable agent_id: {info!r}\n")
            return 20
        agent_name = _str_field(info, "agent_name")
        # v0.8.0: refresh local index with the post-patch state (well-formed identity only).
        if agent_name:
            update_local_agent(
                LocalAgentEntry(
                    agent_id=agent_id,
                    agent_name=agent_name,
                    role=_str_field(info, "role", default="?"),
                    description=make_description(_str_field(info, "system_prompt")),
                    defined_at=_str_field(info, "updated_at"),
                )
            )
        print(f"patched {agent_id}")
        return 0


async def _run_delete(ns: argparse.Namespace) -> int:
    api_key, server_url = _resolve_auth(ns)
    from ratatoskr import wt
    from ratatoskr.local_agents import remove_local_agent

    async with _transport(server_url, api_key) as transport:
        client = wt.build_client(server_url, api_key=api_key, transport=transport)
        await wt.delete_agent(client, ns.agent_id)
        # v0.8.0: drop from local index so the picker stops listing it.
        remove_local_agent(ns.agent_id)
        print(f"deleted {ns.agent_id}")
        return 0


def main(argv: list[str] | None = None) -> int:
    """Sync entry point — argparse + dispatch + error → exit-code mapping.

    Mirrors ratatoskr.cli.main()'s error-routing matrix:
      0  happy
      10 usage error
      11 auth error
      20 api-failure (typed exception or generic SessionApiFailed)
      21 network error
    """
    import asyncio
    import sys

    import worldtree_sdk as wtsdk

    from ratatoskr.wt import SessionApiFailed

    parser = _build_parser()
    try:
        ns = parser.parse_args(argv)
    except SystemExit as exc:
        return int(exc.code) if exc.code is not None else 0

    handler = {
        "define": _run_define,
        "patch": _run_patch,
        "delete": _run_delete,
    }[ns.cmd]

    try:
        return asyncio.run(handler(ns))
    except _Tier3UsageError as exc:
        sys.stderr.write(f"[usage_error] {exc}\n")
        return 10
    except _Tier3AuthError as exc:
        sys.stderr.write(f"[auth_error] {exc}\n")
        return 11
    except AssertionError as exc:
        sys.stderr.write(f"[usage_error] {exc}\n")
        return 10
    except Tier3QuotaExceeded as exc:
        sys.stderr.write(f"[quota_exceeded] retry_after={exc.retry_after}\n")
        return 20
    except Tier3UserIdUnsupported:
        sys.stderr.write("[user_id_unsupported]\n")
        return 20
    except Tier3AgentNotFound as exc:
        sys.stderr.write(f"[agent_not_found] {exc.agent_id}\n")
        return 20
    except Tier3FieldNotMutable as exc:
        sys.stderr.write(f"[field_not_mutable] field={exc.field}\n")
        return 20
    except Tier3LayerDeferred as exc:
        sys.stderr.write(f"[layer_deferred] field={exc.field}\n")
        return 20
    except SessionApiFailed as exc:
        sys.stderr.write(
            f"[api_failed] status={exc.status} "
            f"error_code={exc.error_code!r} body={exc.body!r}\n"
        )
        return 20
    except (
        httpx.ConnectError,
        httpx.ReadTimeout,
        httpx.TransportError,
        wtsdk.ConnectFailed,  # SDK normalizes any pre-response transport failure here
    ) as exc:
        sys.stderr.write(f"[network_error] {type(exc).__name__}: {exc}\n")
        return 21


if __name__ == "__main__":
    import sys

    sys.exit(main())
