"""Local index of tier-3 agents defined via `python -m ratatoskr.tier3`.

Workaround for Worldtree's ``GET /agents`` not returning consumer-defined
agents (the public list excludes tier-3 per-spec; see issue #15 smoke
findings). Local file maintains a list of agent_ids + display metadata so
the picker can show them alongside foundational agents.

Storage shape: JSON at ``$XDG_CONFIG_HOME/ratatoskr/local_agents.json``
(default ``~/.config/ratatoskr/local_agents.json``). Override via
``$RATATOSKR_LOCAL_AGENTS`` env var for tests / per-machine isolation.

If Worldtree later starts returning tier-3 agents in ``GET /agents``, this
module's role narrows to redundant local cache; can be removed cleanly
since the picker's dedup-by-agent-id keeps remote-wins behavior.

Failure modes are lenient: missing file → empty index; corrupt JSON or
schema mismatch → empty index (no crash). The picker continues to show
foundational agents either way; the local-tier-3 surface degrades to
"operator passes --agent ratatoskr:<name> explicitly" — the
pre-v0.8.0 workflow.
"""

from __future__ import annotations

import json
import os
from dataclasses import asdict, dataclass
from pathlib import Path

_SCHEMA_VERSION = 1


@dataclass(frozen=True)
class LocalAgentEntry:
    """One row in the local tier-3 agent index.

    Schema:
      - ``agent_id``: full "user_id:agent_name" string (Worldtree-owned).
      - ``agent_name``: slug from define (display name).
      - ``model``: provider model ID at last define/patch.
      - ``description``: synthetic display string (typically derived from
        the system_prompt's first line + a "(tier 3)" prefix; the picker
        uses this in its ``{id} · {name} — {description}`` rendering).
      - ``defined_at``: ISO-8601 timestamp from the Tier3AgentInfo response.
    """

    agent_id: str
    agent_name: str
    model: str
    description: str
    defined_at: str


def _local_agents_path() -> Path:
    """Resolve the local index file path with XDG + env-var override."""
    override = os.environ.get("RATATOSKR_LOCAL_AGENTS")
    if override:
        return Path(override)
    xdg = os.environ.get("XDG_CONFIG_HOME")
    base = Path(xdg) if xdg else (Path.home() / ".config")
    return base / "ratatoskr" / "local_agents.json"


def load_local_agents() -> list[LocalAgentEntry]:
    """Read the local index. Returns ``[]`` on missing file, corrupt JSON,
    schema mismatch, or any read error — never raises.
    """
    path = _local_agents_path()
    if not path.exists():
        return []
    try:
        raw = json.loads(path.read_text())
    except (json.JSONDecodeError, OSError):
        return []
    if not isinstance(raw, dict) or raw.get("version") != _SCHEMA_VERSION:
        return []
    agents = raw.get("agents", [])
    if not isinstance(agents, list):
        return []
    out: list[LocalAgentEntry] = []
    for item in agents:
        if not isinstance(item, dict):
            continue
        try:
            out.append(LocalAgentEntry(**item))
        except TypeError:
            # Malformed row (missing/extra fields) — skip silently.
            continue
    return out


def _save_local_agents(agents: list[LocalAgentEntry]) -> None:
    """Persist the index. Creates parent dir as needed."""
    path = _local_agents_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    payload = {"version": _SCHEMA_VERSION, "agents": [asdict(a) for a in agents]}
    path.write_text(json.dumps(payload, indent=2))


def add_local_agent(entry: LocalAgentEntry) -> None:
    """Add (or replace) an agent in the local index. agent_id is the key."""
    agents = [a for a in load_local_agents() if a.agent_id != entry.agent_id]
    agents.append(entry)
    _save_local_agents(agents)


def update_local_agent(entry: LocalAgentEntry) -> None:
    """Update an existing entry. Identical semantics to ``add_local_agent``
    (agent_id is the dedup key), exposed separately so callers can
    self-document intent.
    """
    add_local_agent(entry)


def remove_local_agent(agent_id: str) -> None:
    """Remove an entry by agent_id. No-op if absent (idempotent)."""
    agents = [a for a in load_local_agents() if a.agent_id != agent_id]
    _save_local_agents(agents)


def make_description(system_prompt: str) -> str:
    """Synthesize a one-line description for the picker from a system prompt.

    Strategy: first non-empty line, stripped of leading markdown heading
    markers and whitespace, prefixed with "(tier 3) ", truncated to 80
    chars. Falls back to "(tier 3) custom system prompt" if the prompt is
    empty (defensive — define rejects empty prompts at PRE-002).
    """
    for line in system_prompt.splitlines():
        stripped = line.lstrip("# ").strip()
        if stripped:
            label = f"(tier 3) {stripped}"
            return label[:80] + ("…" if len(label) > 80 else "")
    return "(tier 3) custom system prompt"
