#!/usr/bin/env python3
"""adapter_profiles.py — single source of truth for Paperclip adapter capabilities.

Why this exists: switching a Paperclip agent's adapter is NOT a free dropdown
flip. Each adapter family differs on three axes that, if not reconciled, crash
the agent immediately or degrade it silently:

  1. model id ↔ provider — each adapter expects a model from its own provider
     (claude-* / gemini-* / deepseek-* / OpenAI Codex-capable ids). Wrong or
     foreign ids fail or silently fall back to the adapter default.
  2. instructions transport —
       • file_path   adapters pass the instructions file as a *path*
                     (`claude --append-system-prompt-file`); the CLI parses it,
                     so a leading `---` YAML frontmatter is fine.
       • content_arg adapters read the file and splice its *content* into the
                     CLI argument (`gemini --prompt "<content>"`,
                     `hermes chat -q "<content>"`). yargs/arg-parsers read a
                     value starting with `--` as a flag → the run dies within
                     ~2s with "Not enough arguments following: prompt". These
                     adapters therefore require a frontmatter-FREE instructions
                     file (must not start with `--`).
  3. tool availability — claude uses the frontmatter `tools:`/`--allowedTools`;
     gemini reads a GLOBAL `~/.gemini/settings.json` `excludeTools` list (one
     list for ALL gemini agents); deepseek uses `~/.hermes`.

Keeping these facts in one declarative registry makes "migrate any agent to any
adapter" data-driven: adding a future adapter (codex_local, grok_local, ...) is
one new entry here. Consumed by scripts/migrate_agent_adapter.py.

Verified 2026-06-17 against the installed adapter packages
(`@paperclipai/adapter-{claude,gemini,codex}-local`, in-repo
`adapters/deepseek-paperclip-adapter`) and the live Paperclip agents table.
"""
from __future__ import annotations


# tool_config values:
#   "frontmatter"   — tools come from the agent .md frontmatter / --allowedTools
#   "gemini_global" — tools gated by the global ~/.gemini/settings.json excludeTools
#   "hermes"        — tools configured under ~/.hermes (deepseek runtime)
ADAPTER_PROFILES: dict[str, dict] = {
    "claude_local": {
        "provider": "claude",
        "default_model": "claude-opus-4-8",
        "instructions_mode": "file_path",
        "frontmatter_safe": True,
        "tool_config": "frontmatter",
    },
    "gemini_local": {
        "provider": "gemini",
        "default_model": "gemini-3.1-pro-preview",
        "instructions_mode": "content_arg",
        "frontmatter_safe": False,
        "tool_config": "gemini_global",
    },
    "deepseek_local": {
        "provider": "deepseek",
        "default_model": "deepseek-v4-pro",
        "instructions_mode": "content_arg",
        "frontmatter_safe": False,
        "tool_config": "hermes",
    },
    "codex_local": {
        "provider": "codex",
        "default_model": "gpt-5.5",
        "instructions_mode": "content_arg",
        "frontmatter_safe": False,
        "tool_config": "codex_home",
        "model_prefixes": (
            "gpt-",
            "o3",
            "o4",
            "codex-",
        ),
    },
}


class UnknownAdapter(ValueError):
    """Raised when an adapter type has no registered profile."""


def get_profile(adapter_type: str) -> dict:
    """Return the capability profile for an adapter type, or raise UnknownAdapter.

    Fail loud: a typo'd or unregistered adapter must not silently pass through
    migration (that is exactly how the original crash happened)."""
    profile = ADAPTER_PROFILES.get(adapter_type)
    if profile is None:
        known = ", ".join(sorted(ADAPTER_PROFILES))
        raise UnknownAdapter(
            f"אדפטר לא מוכר: {adapter_type!r}. רשומים: {known}. "
            f"הוסף פרופיל ל-adapter_profiles.py לפני מעבר אליו."
        )
    return profile


def model_matches_provider(model: str, adapter_type: str) -> bool:
    """True iff `model` belongs to the adapter's provider family.

    Empty model is treated as matching — the adapter falls back to its own
    default model, which is by definition in-family."""
    if not model:
        return True
    profile = get_profile(adapter_type)
    prefixes = profile.get("model_prefixes")
    if prefixes:
        return model.startswith(tuple(prefixes))
    return model.startswith(profile["provider"])
