"""Shared logic for the /codex-runtime slash command.

Toggles `model.openai_runtime` between "auto" (= chat_completions, Hermes'
default) and "codex_app_server" (= hand turns to a codex subprocess).

Both CLI (cli.py) and gateway (gateway/run.py) call into this module so the
behavior stays identical across surfaces.

The actual runtime resolution happens in hermes_cli.runtime_provider's
_maybe_apply_codex_app_server_runtime() helper, which reads the persisted
config value. This module just persists the value and reports the change.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import Optional

logger = logging.getLogger(__name__)


VALID_RUNTIMES = ("auto", "codex_app_server")


@dataclass
class CodexRuntimeStatus:
    """Result of a /codex-runtime invocation. Callers render this however
    suits their surface (CLI uses Rich panels, gateway sends a text message)."""

    success: bool
    new_value: Optional[str] = None
    old_value: Optional[str] = None
    message: str = ""
    requires_new_session: bool = False
    codex_binary_ok: bool = True
    codex_version: Optional[str] = None


def parse_args(arg_string: str) -> tuple[Optional[str], list[str]]:
    """Parse the slash-command argument string. Returns (value, errors).

    No args         → return current state (value=None)
    'auto' / 'codex_app_server' / 'on' / 'off' → return that value
    anything else   → error
    """
    raw = (arg_string or "").strip().lower()
    if not raw:
        return None, []
    # Accept human-friendly synonyms
    if raw in {"on", "codex", "enable"}:
        return "codex_app_server", []
    if raw in {"off", "default", "disable", "hermes"}:
        return "auto", []
    if raw in VALID_RUNTIMES:
        return raw, []
    return None, [
        f"Unknown runtime {raw!r}. Use one of: auto, codex_app_server, on, off"
    ]


def get_current_runtime(config: dict) -> str:
    """Read the current `model.openai_runtime` value from a config dict.
    Returns 'auto' for unset / empty / unrecognized values."""
    if not isinstance(config, dict):
        return "auto"
    model_cfg = config.get("model") or {}
    if not isinstance(model_cfg, dict):
        return "auto"
    value = str(model_cfg.get("openai_runtime") or "").strip().lower()
    if value in VALID_RUNTIMES:
        return value
    return "auto"


def set_runtime(config: dict, new_value: str) -> str:
    """Mutate the config dict in place to persist the new runtime value.
    Returns the previous value for callers that want to report a delta."""
    if new_value not in VALID_RUNTIMES:
        raise ValueError(
            f"invalid runtime {new_value!r}; must be one of {VALID_RUNTIMES}"
        )
    old = get_current_runtime(config)
    if not isinstance(config.get("model"), dict):
        config["model"] = {}
    config["model"]["openai_runtime"] = new_value
    return old


def check_codex_binary_ok() -> tuple[bool, Optional[str]]:
    """Best-effort verification that codex CLI is installed at acceptable
    version. Returns (ok, version_or_message)."""
    try:
        from agent.transports.codex_app_server import check_codex_binary

        return check_codex_binary()
    except Exception as exc:  # pragma: no cover
        return False, f"codex check failed: {exc}"


def apply(
    config: dict,
    new_value: Optional[str],
    *,
    persist_callback=None,
) -> CodexRuntimeStatus:
    """Top-level entry point used by both CLI and gateway handlers.

    Args:
        config: in-memory config dict (will be mutated when new_value is set)
        new_value: desired runtime; None means "show current state only"
        persist_callback: optional callable taking the mutated config dict
            and persisting it to disk. Skipped when None (used by tests).

    Returns: CodexRuntimeStatus describing the outcome.
    """
    current = get_current_runtime(config)

    # Cache the codex binary check for this apply() call. Subprocess spawn
    # is cheap (~50ms for `codex --version`), but we'd otherwise call it up
    # to 3 times in the enable path (read-only/state, gate, success message).
    # None = not yet checked; (bool, str) = result.
    _binary_check: Optional[tuple[bool, Optional[str]]] = None

    def _check_binary_cached() -> tuple[bool, Optional[str]]:
        nonlocal _binary_check
        if _binary_check is None:
            _binary_check = check_codex_binary_ok()
        return _binary_check

    # Read-only call: just report state
    if new_value is None:
        ok, ver = _check_binary_cached()
        msg = (
            f"openai_runtime: {current}\n"
            f"codex CLI: {'OK ' + ver if ok else 'not available — ' + (ver or 'install with `npm i -g @openai/codex`')}"
        )
        return CodexRuntimeStatus(
            success=True,
            new_value=current,
            old_value=current,
            message=msg,
            codex_binary_ok=ok,
            codex_version=ver if ok else None,
        )

    # No change requested
    if new_value == current:
        return CodexRuntimeStatus(
            success=True,
            new_value=current,
            old_value=current,
            message=f"openai_runtime already set to {current}",
        )

    # If switching ON, verify codex CLI is installed before persisting —
    # an opt-in toggle that silently fails on the first turn is the
    # worst possible UX. Block here with a clear install hint.
    if new_value == "codex_app_server":
        ok, ver_or_msg = _check_binary_cached()
        if not ok:
            return CodexRuntimeStatus(
                success=False,
                new_value=None,
                old_value=current,
                message=(
                    "Cannot enable codex_app_server runtime: "
                    f"{ver_or_msg or 'codex CLI not available'}\n"
                    "Install with: npm i -g @openai/codex"
                ),
                codex_binary_ok=False,
                codex_version=None,
            )

    set_runtime(config, new_value)
    if persist_callback is not None:
        try:
            persist_callback(config)
        except Exception as exc:
            logger.exception("failed to persist openai_runtime change")
            return CodexRuntimeStatus(
                success=False,
                new_value=new_value,
                old_value=current,
                message=f"updated config in memory but persist failed: {exc}",
            )

    msg_lines = [
        f"openai_runtime: {current} → {new_value}",
    ]
    if new_value == "codex_app_server":
        ok, ver = _check_binary_cached()
        if ok:
            msg_lines.append(f"codex CLI: {ver}")
        # Auto-migrate Hermes' MCP servers + Codex's installed curated
        # plugins into ~/.codex/config.toml so the spawned codex subprocess
        # sees the same tool surface AND can call back into Hermes for
        # browser/web/delegate_task/vision/memory tools (#7 fix).
        # Failures are non-fatal — the runtime change still proceeds.
        try:
            from hermes_cli.codex_runtime_plugin_migration import migrate
            mig_report = migrate(config)
            # Tools/MCP servers (excluding the hermes-tools callback,
            # which is internal plumbing — surface separately).
            user_servers = [
                s for s in mig_report.migrated if s != "hermes-tools"
            ]
            if user_servers:
                msg_lines.append(
                    f"Migrated {len(user_servers)} MCP server(s): "
                    f"{', '.join(user_servers)}"
                )
            # Native Codex plugin migration (Linear, GitHub, etc.)
            if mig_report.migrated_plugins:
                msg_lines.append(
                    f"Migrated {len(mig_report.migrated_plugins)} native "
                    f"Codex plugin(s): {', '.join(mig_report.migrated_plugins)}"
                )
            elif mig_report.plugin_query_error:
                msg_lines.append(
                    f"Codex plugin discovery skipped: "
                    f"{mig_report.plugin_query_error}"
                )
            # Permissions + Hermes tool callback are always-on production
            # bits the user benefits from knowing about.
            if mig_report.wrote_permissions_default:
                msg_lines.append(
                    f"Default sandbox: {mig_report.wrote_permissions_default} "
                    f"(no approval prompt on every write)"
                )
            if "hermes-tools" in mig_report.migrated:
                msg_lines.append(
                    "Hermes tool callback registered: codex can now use "
                    "web_search, web_extract, browser_*, vision_analyze, "
                    "image_generate, skill_view, skills_list, text_to_speech, "
                    "kanban_* (worker + orchestrator) via MCP."
                )
                msg_lines.append(
                    "  (delegate_task, memory, session_search, todo run "
                    "only on the default Hermes runtime — they need the "
                    "agent loop context.)"
                )
            msg_lines.append(f"  (config: {mig_report.target_path})")
            for err in mig_report.errors:
                msg_lines.append(f"⚠ MCP migration: {err}")
        except Exception as exc:
            msg_lines.append(f"⚠ MCP migration skipped: {exc}")
        msg_lines.append(
            "OpenAI/Codex turns now run through `codex app-server` "
            "(terminal/file ops/patching inside Codex; "
            "Hermes tools available via MCP callback)."
        )
        msg_lines.append(
            "Effective on next session — current cached agent keeps "
            "the prior runtime to preserve prompt cache."
        )
    else:
        msg_lines.append("OpenAI/Codex turns will use the default Hermes runtime.")
        msg_lines.append("Effective on next session.")
    return CodexRuntimeStatus(
        success=True,
        new_value=new_value,
        old_value=current,
        message="\n".join(msg_lines),
        requires_new_session=True,
    )
