"""Slash command handlers — extracted from engine.py for maintainability."""

from __future__ import annotations

import asyncio
import json
from typing import Dict

from salmalm.constants import VERSION, COMMAND_MODEL, MODEL_ALIASES as _CONST_ALIASES  # noqa: F401

MODEL_ALIASES = {"auto": None, **_CONST_ALIASES}
from salmalm.core.cost import estimate_tokens, estimate_cost, MODEL_PRICING, get_pricing as _get_pricing  # noqa: F401
from salmalm.security.crypto import log  # noqa: F401


def _get_engine():
    """Lazy import to avoid circular dependency."""
    from salmalm.core.engine import _engine

    return _engine


def _get_router():
    """Lazy import router to avoid circular dependency."""
    from salmalm.core import router

    return router


def _lazy_compact_messages():
    from salmalm.core import compact_messages

    return compact_messages


def _lazy_prune_context():
    from salmalm.core.session_manager import prune_context

    return prune_context


def _lazy_build_system_prompt(**kwargs):
    from salmalm.core.prompt import build_system_prompt

    return build_system_prompt(**kwargs)


# Session usage tracking (shared state)
_session_usage: Dict[str, dict] = {}


def _get_session_usage(session_id: str) -> dict:
    if session_id not in _session_usage:
        _session_usage[session_id] = {"responses": [], "mode": "off", "total_cost": 0.0}
    return _session_usage[session_id]


def record_response_usage(session_id: str, model: str, usage: dict) -> None:
    """Record per-response usage for /usage command."""
    su = _get_session_usage(session_id)
    cost = estimate_cost(model, usage)
    su["responses"].append(
        {
            "model": model,
            "input": usage.get("input", 0),
            "output": usage.get("output", 0),
            "cache_read": usage.get("cache_read_input_tokens", 0),
            "cache_write": usage.get("cache_creation_input_tokens", 0),
            "cost": cost,
        }
    )
    su["total_cost"] += cost


def _cmd_clear(cmd, session, **_):
    session.messages = [m for m in session.messages if m["role"] == "system"][:1]
    return "Conversation cleared."


def _cmd_help(cmd, session, **_):
    from salmalm.tools import TOOL_DEFINITIONS

    tool_count = len(TOOL_DEFINITIONS)
    return f"""😈 **SalmAlm v{VERSION}** — Personal AI Gateway

📌 **Commands**
/clear — Clear conversation
/help — This help
/model <name> — Change model
/think <question> — 🧠 Deep reasoning (Opus)
/plan <question> — 📋 Plan → Execute
/status — Usage + Cost
/context — Context window token usage
/tools — Tool list
/uptime — Uptime stats (업타임)
/latency — Latency stats (레이턴시)
/health detail — Detailed health report (상세 헬스)
/security — 🛡️ Security audit report
/subagents — 🤖 Sub-agents (spawn|list|stop|steer|log|info|collect)
/evolve — 🧬 Self-evolving prompt (status|apply|reset|history)
/mood — 🎭 Mood-aware response (status|on|off|sensitive)
/think <내용> — 💭 Record a thought (or list|search|tag|stats|export)

🤖 **Model Aliases** (27)
claude, sonnet, opus, haiku, gpt, gpt5, o3, o4mini,
grok, grok4, gemini, flash, deepseek, llama, auto ...

🔧 **Tools** ({tool_count})
File R/W, code exec, web search, RAG search,
system monitor, cron jobs, image analysis, TTS ...

🧠 **Intelligence Engine**
Auto intent classification (7 levels) → Model routing → Parallel tools → Self-evaluation

💡 **Tip**: Just speak naturally. Read a file, search the web, write code, etc."""


def _cmd_status(cmd, session, **_):
    from salmalm.tools.tool_handlers import execute_tool

    return execute_tool("usage_report", {})


def _cmd_tools(cmd, session, **_):
    from salmalm.tools import TOOL_DEFINITIONS

    lines = [f"🔧 **Tool List** ({len(TOOL_DEFINITIONS)})\n"]
    for t in TOOL_DEFINITIONS:
        lines.append(f"• **{t['name']}** — {t['description'][:60]}")
    return "\n".join(lines)


async def _cmd_think(cmd, session, *, on_tool=None, **_):
    think_msg = cmd[7:].strip()
    if not think_msg:
        return "Usage: /think <question>"
    # Route thought-stream subcommands
    _thought_subs = ("list", "search", "tag", "timeline", "stats", "export")
    first_word = think_msg.split(None, 1)[0].lower() if think_msg else ""
    if first_word in _thought_subs:
        return _cmd_thought(cmd, session)
    session.add_user(think_msg)
    session.messages = _lazy_compact_messages()(session.messages, session=session)
    classification = {"intent": "analysis", "tier": 3, "thinking": True, "thinking_budget": 16000, "score": 5}
    return await _get_engine().run(
        session,
        think_msg,
        model_override=COMMAND_MODEL,  # noqa: E128
        on_tool=on_tool,
        classification=classification,
    )  # noqa: E128


async def _cmd_plan(cmd, session, *, model_override=None, on_tool=None, **_):
    plan_msg = cmd[6:].strip()
    if not plan_msg:
        return "Usage: /plan <task description>"
    session.add_user(plan_msg)
    session.messages = _lazy_compact_messages()(session.messages, session=session)
    classification = {"intent": "code", "tier": 3, "thinking": True, "thinking_budget": 10000, "score": 5}
    return await _get_engine().run(
        session, plan_msg, model_override=model_override, on_tool=on_tool, classification=classification
    )  # noqa: E128


def _cmd_uptime(cmd, session, **_):
    from salmalm.features.sla import uptime_monitor, sla_config  # noqa: F401

    stats = uptime_monitor.get_stats()
    target = stats["target_pct"]
    pct = stats["monthly_uptime_pct"]
    status_icon = "🟢" if pct >= target else ("🟡" if pct >= 99.0 else "🔴")
    lines = [
        "📊 **SalmAlm Uptime** / 업타임 현황\n",
        f"{status_icon} Current uptime: **{stats['uptime_human']}**",
        f"📅 Month ({stats['month']}): **{pct}%** (target: {target}%)",
        f"📅 Today: **{stats['daily_uptime_pct']}%**",
        f"🕐 Started: {stats['start_time'][:19]}",
    ]
    incidents = stats.get("recent_incidents", [])
    if incidents:
        lines.append(f"\n⚠️ Recent incidents ({len(incidents)}):")
        for inc in incidents[:5]:
            dur = f"{inc['duration_sec']:.0f}s" if inc["duration_sec"] else "?"
            lines.append(f"  • {inc['start'][:19]} — {inc['reason']} ({dur})")
    return "\n".join(lines)


def _cmd_latency(cmd, session, **_):
    from salmalm.features.sla import latency_tracker

    stats = latency_tracker.get_stats()
    if stats["count"] == 0:
        return "📊 No latency data yet. / 레이턴시 데이터가 없습니다."
    tgt = stats["targets"]
    ttft = stats["ttft"]
    total = stats["total"]
    ttft_ok = "✅" if ttft["p95"] <= tgt["ttft_ms"] else "⚠️"
    total_ok = "✅" if total["p95"] <= tgt["response_ms"] else "⚠️"
    lines = [
        f"📊 **Latency Stats** / 레이턴시 통계 ({stats['count']} requests)\n",
        f"{ttft_ok} **TTFT** (Time To First Token):",
        f"  P50={ttft['p50']:.0f}ms  P95={ttft['p95']:.0f}ms  P99={ttft['p99']:.0f}ms  (target: <{tgt['ttft_ms']}ms)",
        f"{total_ok} **Total Response Time**:",
        f"  P50={total['p50']:.0f}ms  P95={total['p95']:.0f}ms  P99={total['p99']:.0f}ms  (target: <{tgt['response_ms']}ms)",
    ]
    if stats["consecutive_timeouts"] > 0:
        lines.append(f"⚠️ Consecutive timeouts: {stats['consecutive_timeouts']}")
    return "\n".join(lines)


def _cmd_health_detail(cmd, session, **_):
    from salmalm.features.sla import watchdog

    report = watchdog.get_detailed_health()
    status = report.get("status", "unknown")
    icon = {"healthy": "🟢", "degraded": "🟡", "unhealthy": "🔴"}.get(status, "⚪")
    lines = [f"{icon} **Health Report** / 상세 헬스 리포트\n", f"Status: **{status}**\n"]
    for name, check in report.get("checks", {}).items():
        s = check.get("status", "?")
        ci = {"ok": "✅", "warning": "⚠️", "error": "❌"}.get(s, "❔")
        extra = ""
        if "usage_mb" in check:
            extra = f" ({check['usage_mb']}MB/{check['limit_mb']}MB)"
        elif "usage_pct" in check:
            extra = f" ({check['usage_pct']}%/{check['limit_pct']}%)"
        elif "error" in check:
            extra = f" ({check['error'][:50]})"
        lines.append(f"{ci} {name}: {s}{extra}")
    return "\n".join(lines)


def _cmd_prune(cmd, session, **_):
    _, stats = _lazy_prune_context()(session.messages)
    total = stats["soft_trimmed"] + stats["hard_cleared"] + stats["unchanged"]
    return (
        f"🧹 **Session Pruning Results**\n"
        f"• Soft-trimmed: {stats['soft_trimmed']}\n"
        f"• Hard-cleared: {stats['hard_cleared']}\n"
        f"• Unchanged: {stats['unchanged']}\n"
        f"• Total tool results scanned: {total}"
    )


def _cmd_usage_daily(cmd, session, **_):
    from salmalm.features.edge_cases import usage_tracker

    report = usage_tracker.daily_report()
    if not report:
        return "📊 No usage data yet. / 아직 사용량 데이터가 없습니다."
    lines = ["📊 **Daily Usage Report / 일별 사용량**\n"]
    for r in report[:14]:
        lines.append(
            f"• {r['date']} | {r['model'].split('/')[-1]} | "
            f"in:{r['input_tokens']} out:{r['output_tokens']} | "
            f"${r['cost']:.4f} ({r['calls']} calls)"
        )
    return "\n".join(lines)


def _cmd_usage_monthly(cmd, session, **_):
    from salmalm.features.edge_cases import usage_tracker

    report = usage_tracker.monthly_report()
    if not report:
        return "📊 No usage data yet. / 아직 사용량 데이터가 없습니다."
    lines = ["📊 **Monthly Usage Report / 월별 사용량**\n"]
    for r in report:
        lines.append(
            f"• {r['month']} | {r['model'].split('/')[-1]} | "
            f"in:{r['input_tokens']} out:{r['output_tokens']} | "
            f"${r['cost']:.4f} ({r['calls']} calls)"
        )
    return "\n".join(lines)


def _cmd_bookmarks(cmd, session, **_):
    from salmalm.features.edge_cases import bookmark_manager

    bms = bookmark_manager.list_all(limit=20)
    if not bms:
        return "⭐ No bookmarks yet. / 아직 북마크가 없습니다."
    lines = ["⭐ **Bookmarks / 북마크**\n"]
    for b in bms:
        lines.append(
            f"• [{b['session_id']}#{b['message_index']}] "
            f"{b['preview'][:60]}{'...' if len(b.get('preview', '')) > 60 else ''}"
        )
    return "\n".join(lines)


def _cmd_compare(cmd, session, *, session_id="", **_):
    compare_msg = cmd[9:].strip()
    if not compare_msg:
        return "Usage: /compare <message> — Compare responses from multiple models"
    from salmalm.features.edge_cases import compare_models

    try:
        loop = asyncio.get_event_loop()
        if loop.is_running():
            import concurrent.futures

            with concurrent.futures.ThreadPoolExecutor() as pool:
                results = pool.submit(lambda: asyncio.run(compare_models(session_id, compare_msg))).result()
        else:
            results = loop.run_until_complete(compare_models(session_id, compare_msg))
    except Exception:
        results = asyncio.run(compare_models(session_id, compare_msg))
    lines = ["🔀 **Model Comparison / 모델 비교**\n"]
    for r in results:
        model_name = r["model"].split("/")[-1]
        if r.get("error"):
            lines.append(f"### ❌ {model_name}\n{r['error']}\n")
        else:
            lines.append(f"### 🤖 {model_name} ({r['time_ms']}ms)\n{r['response'][:500]}\n")
    return "\n".join(lines)


def _cmd_security(cmd, session, **_):
    from salmalm.security import security_auditor

    return security_auditor.format_report()


def _cmd_context(cmd, session, **_):
    """Show context window token usage breakdown."""
    sub = cmd.strip().split()
    detail_mode = len(sub) > 1 and sub[1] == "detail"

    from salmalm.core.prompt import build_system_prompt

    sys_prompt = build_system_prompt(full=False)
    sys_tokens = estimate_tokens(sys_prompt)

    # Tool schemas
    tool_tokens = 0
    tool_text = ""
    tool_details = []
    try:
        from salmalm.tools import TOOL_DEFINITIONS

        for t in TOOL_DEFINITIONS:
            schema_text = json.dumps(
                {"name": t["name"], "description": t["description"], "input_schema": t["input_schema"]}
            )
            tool_details.append((t["name"], len(schema_text), estimate_tokens(schema_text)))
        tool_text = json.dumps(
            [
                {"name": t["name"], "description": t["description"], "input_schema": t["input_schema"]}
                for t in TOOL_DEFINITIONS
            ]
        )
        tool_tokens = estimate_tokens(tool_text)
    except Exception:
        TOOL_DEFINITIONS = []

    # Injected files breakdown
    from salmalm.constants import SOUL_FILE, AGENTS_FILE, MEMORY_FILE, USER_FILE, BASE_DIR
    from salmalm.core.prompt import USER_SOUL_FILE

    file_details = []
    for label, path in [
        ("SOUL.md", SOUL_FILE),
        ("USER_SOUL.md", USER_SOUL_FILE),
        ("AGENTS.md", AGENTS_FILE),
        ("MEMORY.md", MEMORY_FILE),
        ("USER.md", USER_FILE),
        ("TOOLS.md", BASE_DIR / "TOOLS.md"),
    ]:
        if path.exists():
            raw = path.read_text(encoding="utf-8")
            file_details.append((label, len(raw), estimate_tokens(raw)))

    # Conversation history
    history_text = ""
    for m in session.messages:
        c = m.get("content", "")
        if isinstance(c, str):
            history_text += c
        elif isinstance(c, list):
            for block in c:
                if isinstance(block, dict):
                    history_text += block.get("content", "") or block.get("text", "") or ""
    history_tokens = estimate_tokens(history_text)

    total = sys_tokens + tool_tokens + history_tokens

    lines = [
        f"""📊 **Context Window Usage**

| Component | Chars | ~Tokens |
|-----------|------:|--------:|
| System Prompt | {len(sys_prompt):,} | {sys_tokens:,} |
| Tool Schemas ({len(TOOL_DEFINITIONS)}) | {len(tool_text):,} | {tool_tokens:,} |
| Conversation ({len(session.messages)} msgs) | {len(history_text):,} | {history_tokens:,} |
| **Total** | | **{total:,}** |"""
    ]

    if detail_mode:
        lines.append("\n📁 **Injected Files**")
        for label, chars, tokens in sorted(file_details, key=lambda x: -x[2]):
            lines.append(f"  • {label}: {chars:,} chars / ~{tokens:,} tokens")

        lines.append("\n🔧 **Tool Schemas (top 10 by size)**")
        for name, chars, tokens in sorted(tool_details, key=lambda x: -x[2])[:10]:
            lines.append(f"  • {name}: {chars:,} chars / ~{tokens:,} tokens")

    lines.append("\n💡 Intent-based injection reduces tools to ≤15 per call.")
    lines.append("🔒 Prompt caching: system prompt + tool schemas marked ephemeral.")
    return "\n".join(lines)


def _cmd_usage(cmd, session, *, session_id="", **_):
    """Handle /usage tokens|full|cost|off commands."""
    parts = cmd.strip().split()
    sub = parts[1] if len(parts) > 1 else "tokens"
    su = _get_session_usage(session_id)

    if sub == "off":
        su["mode"] = "off"
        return "📊 Usage footer: **OFF**"
    elif sub == "tokens":
        su["mode"] = "tokens"
        if not su["responses"]:
            return "📊 Usage tracking: **ON** (tokens mode). No responses yet."
        last = su["responses"][-1]
        return (
            f"📊 Usage mode: **tokens**\n"
            f"Last: in={last['input']:,} out={last['output']:,} "
            f"(cache_read={last['cache_read']:,} cache_write={last['cache_write']:,})"
        )
    elif sub == "full":
        su["mode"] = "full"
        if not su["responses"]:
            return "📊 Usage tracking: **ON** (full mode). No responses yet."
        lines = ["📊 **Usage (full)**\n"]
        for i, r in enumerate(su["responses"][-10:], 1):
            model_short = r["model"].split("/")[-1][:20]
            lines.append(f"{i}. {model_short} | in:{r['input']:,} out:{r['output']:,} | ${r['cost']:.4f}")
        lines.append(f"\n💰 Session total: **${su['total_cost']:.4f}**")
        return "\n".join(lines)
    elif sub == "cost":
        lines = ["💰 **Session Cost Summary**\n"]
        if not su["responses"]:
            lines.append("No API calls yet.")
        else:
            lines.append(f"Requests: {len(su['responses'])}")
            total_in = sum(r["input"] for r in su["responses"])
            total_out = sum(r["output"] for r in su["responses"])
            total_cache_read = sum(r["cache_read"] for r in su["responses"])
            total_cache_write = sum(r["cache_write"] for r in su["responses"])
            lines.append(
                f"Input tokens: {total_in:,} (cache read: {total_cache_read:,}, cache write: {total_cache_write:,})"
            )
            lines.append(f"Output tokens: {total_out:,}")
            lines.append(f"**Total cost: ${su['total_cost']:.4f}**")
            if total_cache_read > 0:
                # Estimate savings from cache
                pricing = _get_pricing(su["responses"][-1]["model"])
                saved = total_cache_read * (pricing["input"] - pricing["cache_read"]) / 1_000_000
                lines.append(f"💡 Cache savings: ~${saved:.4f}")
        return "\n".join(lines)
    else:
        return "📊 `/usage tokens|full|cost|off`"


def _cmd_soul(cmd, session, **_):
    from salmalm.core.prompt import get_user_soul, USER_SOUL_FILE

    content = get_user_soul()
    if content:
        return f"📜 **SOUL.md** (`{USER_SOUL_FILE}`)\n\n{content}"
    return f"📜 SOUL.md is not set. Create `{USER_SOUL_FILE}` or edit via Settings."


def _cmd_soul_reset(cmd, session, **_):
    from salmalm.core.prompt import reset_user_soul, build_system_prompt

    reset_user_soul()
    session.add_system(build_system_prompt(full=True))
    return "📜 SOUL.md reset to default."


def _cmd_model(cmd, session, **_):
    model_name = cmd[7:].strip() if len(cmd) > 7 else ""
    if not model_name:
        current = getattr(session, "model_override", None) or "auto"
        return f"Current model: **{current}**\nUsage: `/model <name>` — e.g. `/model opus`, `/model auto`, `/model anthropic/claude-sonnet-4-6`"
    if model_name in ("auto", "opus", "sonnet", "haiku"):
        session.model_override = model_name if model_name != "auto" else "auto"
        if model_name == "auto":
            _get_router().set_force_model(None)
            return "Model: **auto** (cost-optimized routing) — saved ✅\n• simple → haiku ⚡ • moderate → sonnet • complex → opus 💎"
        labels = {"opus": "claude-opus-4-6 💎", "sonnet": "claude-sonnet-4-6", "haiku": "claude-haiku-4-5 ⚡"}
        return f"Model: **{model_name}** ({labels[model_name]}) — saved ✅"
    if "/" in model_name:
        _get_router().set_force_model(model_name)
        session.model_override = model_name
        return f"Model changed: {model_name} — saved ✅"
    if model_name in MODEL_ALIASES:
        resolved = MODEL_ALIASES[model_name]
        _get_router().set_force_model(resolved)
        session.model_override = resolved
        return f"Model changed: {model_name} → {resolved} — saved ✅"
    return (
        f"Unknown model: {model_name}\\nAvailable: auto, opus, sonnet, haiku, {', '.join(sorted(MODEL_ALIASES.keys()))}"
    )


def _cmd_tts(cmd, session, **_):
    arg = cmd[4:].strip()
    if arg == "on":
        session.tts_enabled = True
        return "🔊 TTS: **ON** — 응답을 음성으로 전송합니다."
    elif arg == "off":
        session.tts_enabled = False
        return "🔇 TTS: **OFF**"
    else:
        status = "ON" if getattr(session, "tts_enabled", False) else "OFF"
        voice = getattr(session, "tts_voice", "alloy")
        return f"🔊 TTS: **{status}** (voice: {voice})\n`/tts on` · `/tts off` · `/voice alloy|nova|echo|fable|onyx|shimmer`"


def _cmd_voice(cmd, session, **_):
    arg = cmd[6:].strip()
    valid_voices = ("alloy", "nova", "echo", "fable", "onyx", "shimmer")
    if arg in valid_voices:
        session.tts_voice = arg
        return f"🎙️ Voice: **{arg}** — saved ✅"
    return f"Available voices: {', '.join(valid_voices)}"


def _cmd_subagents(cmd, session, **_):
    """Handle /subagents commands: list, spawn, stop, steer, log, info, collect."""
    from salmalm.features.agents import SubAgent

    parts = cmd.split(maxsplit=2)
    sub = parts[1] if len(parts) > 1 else "list"
    arg = parts[2] if len(parts) > 2 else ""

    if sub == "list":
        agents = SubAgent.list_agents()
        if not agents:
            return "🤖 No active sub-agents."
        lines = ["🤖 **Sub-agents**\n"]
        for i, a in enumerate(agents, 1):
            icon = {"running": "🔄", "completed": "✅", "error": "❌", "stopped": "⏹"}.get(
                a["status"].split(".")[0], "❓"
            )
            lines.append(
                f"{icon} #{i} `{a['id']}` — {a['label']} [{a['status']}] "
                f"({a['runtime_s']}s, ${a.get('estimated_cost', 0):.4f})"
            )
        return "\n".join(lines)

    elif sub == "spawn":
        if not arg:
            return "❌ Usage: /subagents spawn <task description>"
        # Parse optional --model flag
        model = None
        if " --model " in arg:
            arg, _, model = arg.rpartition(" --model ")
            model = model.strip() or None
        agent_id = SubAgent.spawn(arg.strip(), model=model)
        return f"🤖 Sub-agent spawned: `{agent_id}`\nTask: {arg[:100]}\nWill notify on completion."

    elif sub == "stop":
        if not arg:
            return "❌ Usage: /subagents stop <id|#N|all>"
        return SubAgent.stop_agent(arg)

    elif sub == "steer":
        # OpenClaw-style: send guidance to a running/completed sub-agent
        steer_parts = arg.split(maxsplit=1)
        agent_id = steer_parts[0] if steer_parts else ""
        message = steer_parts[1] if len(steer_parts) > 1 else ""
        if not agent_id or not message:
            return "❌ Usage: /subagents steer <id|#N> <message>"
        # Try subagent_manager first, fall back to SubAgent.send_message
        try:
            from salmalm.features.subagents import subagent_manager

            return subagent_manager.steer(agent_id, message)
        except Exception:
            return SubAgent.send_message(agent_id, message)

    elif sub == "log":
        log_parts = arg.split(maxsplit=1)
        agent_id = log_parts[0] if log_parts else ""
        limit = int(log_parts[1]) if len(log_parts) > 1 and log_parts[1].isdigit() else 20
        if not agent_id:
            return "❌ Usage: /subagents log <id|#N> [limit]"
        return SubAgent.get_log(agent_id, limit)

    elif sub == "info":
        if not arg:
            return "❌ Usage: /subagents info <id|#N>"
        return SubAgent.get_info(arg)

    elif sub == "collect":
        # OpenClaw-style: collect all completed results
        from salmalm.features.subagents import subagent_manager

        results = subagent_manager.collect_results(parent_session="web")
        if not results:
            return "📋 No uncollected sub-agent results."
        lines = ["📋 **Completed Sub-agent Results**\n"]
        for r in results:
            lines.append(
                f"✅ `{r['task_id']}` — {r['description']}\n"
                f"   {r['result'][:300]}{'...' if len(r.get('result', '')) > 300 else ''}"
            )
        return "\n".join(lines)

    return "❌ Usage: /subagents spawn|list|stop|steer|log|info|collect <args>"


def _cmd_agent(cmd, session, *, session_id="", **_):
    from salmalm.features.agents import agent_manager

    parts = cmd.split(maxsplit=2)
    sub = parts[1] if len(parts) > 1 else "list"
    if sub == "list":
        agents = agent_manager.list_agents()
        lines = ["🤖 **Agents** (에이전트 목록)\n"]
        for a in agents:
            lines.append(f"• **{a['id']}** — {a['display_name']}")
        bindings = agent_manager.list_bindings()
        if bindings:
            lines.append("\n📌 **Bindings** (바인딩)")
            for k, v in bindings.items():
                lines.append(f"• {k} → {v}")
        return "\n".join(lines)
    elif sub == "create" and len(parts) > 2:
        return agent_manager.create(parts[2])
    elif sub == "switch" and len(parts) > 2:
        chat_key = f"session:{session_id}"
        return agent_manager.switch(chat_key, parts[2])
    elif sub == "delete" and len(parts) > 2:
        return agent_manager.delete(parts[2])
    elif sub == "bind" and len(parts) > 2:
        bind_parts = parts[2].split()
        if len(bind_parts) == 2:
            return agent_manager.bind(bind_parts[0], bind_parts[1])
        return "❌ Usage: /agent bind <chat_key> <agent_id>"
    return "❌ Usage: /agent list|create|switch|delete|bind <args>"


def _cmd_hooks(cmd, session, **_):
    from salmalm.features.hooks import hook_manager

    parts = cmd.split(maxsplit=2)
    sub = parts[1] if len(parts) > 1 else "list"
    if sub == "list":
        hooks = hook_manager.list_hooks()
        if not hooks:
            return "📋 No hooks configured. Edit ~/.salmalm/hooks.json"
        lines = ["🪝 **Hooks** (이벤트 훅)\n"]
        for event, info in hooks.items():
            cmds_list = info["commands"]
            pc = info["plugin_callbacks"]
            lines.append(f"• **{event}**: {len(cmds_list)} commands, {pc} plugin callbacks")
            for i, c in enumerate(cmds_list):
                lines.append(f"  [{i}] `{c[:60]}`")
        return "\n".join(lines)
    elif sub == "test" and len(parts) > 2:
        return hook_manager.test_hook(parts[2].strip())
    elif sub == "add" and len(parts) > 2:
        add_parts = parts[2].split(maxsplit=1)
        if len(add_parts) == 2:
            return hook_manager.add_hook(add_parts[0], add_parts[1])
        return "❌ Usage: /hooks add <event> <command>"
    elif sub == "reload":
        hook_manager.reload()
        return "🔄 Hooks reloaded"
    return "❌ Usage: /hooks list|test|add|reload"


def _cmd_plugins(cmd, session, **_):
    from salmalm.features.plugin_manager import plugin_manager

    parts = cmd.split(maxsplit=2)
    sub = parts[1] if len(parts) > 1 else "list"
    if sub == "list":
        plugins = plugin_manager.list_plugins()
        if not plugins:
            return "🔌 No plugins found. Add to ~/.salmalm/plugins/"
        lines = ["🔌 **Plugins** (플러그인)\n"]
        for p in plugins:
            status = "✅" if p["enabled"] else "❌"
            err = f" ⚠️ {p['error']}" if p.get("error") else ""
            lines.append(f"• {status} **{p['name']}** v{p['version']} — {p['description'][:40]}{err}")
            if p["tools"]:
                lines.append(f"  Tools: {', '.join(p['tools'])}")
        return "\n".join(lines)
    elif sub == "reload":
        return plugin_manager.reload_all()
    elif sub == "enable" and len(parts) > 2:
        return plugin_manager.enable(parts[2].strip())
    elif sub == "disable" and len(parts) > 2:
        return plugin_manager.disable(parts[2].strip())
    return "❌ Usage: /plugins list|reload|enable|disable <name>"


# ── Self-Evolving Prompt commands ──
def _cmd_evolve(cmd, session, **_):
    parts = cmd.strip().split(None, 2)
    sub = parts[1] if len(parts) > 1 else "status"
    from salmalm.features.self_evolve import prompt_evolver

    if sub == "status":
        return prompt_evolver.get_status()
    elif sub == "apply":
        from salmalm.core.prompt import USER_SOUL_FILE

        return prompt_evolver.apply_to_soul(USER_SOUL_FILE)
    elif sub == "reset":
        return prompt_evolver.reset()
    elif sub == "history":
        return prompt_evolver.get_history()
    return "❌ Usage: /evolve status|apply|reset|history"


# ── Mood-Aware commands ──


def _cmd_mood(cmd, session, **_):
    parts = cmd.strip().split(None, 2)
    sub = parts[1] if len(parts) > 1 else "status"
    from salmalm.features.mood import mood_detector

    if sub == "status":
        # Use last user message for context
        last_msg = ""
        for m in reversed(session.messages):
            if m.get("role") == "user":
                last_msg = str(m.get("content", ""))
                break
        return mood_detector.get_status(last_msg)
    elif sub in ("off", "on", "sensitive"):
        return mood_detector.set_mode(sub)
    elif sub == "report":
        period = parts[2] if len(parts) > 2 else "week"
        return mood_detector.generate_report(period)
    return "❌ Usage: /mood status|off|on|sensitive|report [week|month]"


# ── Thought Stream commands ──


def _cmd_thought(cmd, session, **_):
    from salmalm.features.thoughts import thought_stream, _format_thoughts, _format_stats

    text = cmd.strip()
    # Remove /think prefix
    if text.startswith("/thought"):
        text = text[8:].strip()
    elif text.startswith("/think"):
        # Only handle /think subcommands here, not /think <question> for deep reasoning
        text = text[6:].strip()

    if not text:
        return "❌ Usage: /think <내용> | /think list | /think search <쿼리> | /think tag <태그> | /think stats"

    parts = text.split(None, 1)
    sub = parts[0]
    arg = parts[1].strip() if len(parts) > 1 else ""

    if sub == "list":
        n = int(arg) if arg.isdigit() else 10
        thoughts = thought_stream.list_recent(n)
        return _format_thoughts(thoughts, f"💭 **최근 {n}개 생각**\n")
    elif sub == "search":
        if not arg:
            return "❌ Usage: /think search <쿼리>"
        results = thought_stream.search(arg)
        return _format_thoughts(results, f"🔍 **검색: {arg}**\n")
    elif sub == "tag":
        if not arg:
            return "❌ Usage: /think tag <태그>"
        results = thought_stream.by_tag(arg)
        return _format_thoughts(results, f"🏷️ **태그: #{arg}**\n")
    elif sub == "timeline":
        results = thought_stream.timeline(arg if arg else None)
        date_label = arg if arg else "오늘"
        return _format_thoughts(results, f"📅 **타임라인: {date_label}**\n")
    elif sub == "stats":
        return _format_stats(thought_stream.stats())
    elif sub == "export":
        md = thought_stream.export_markdown()
        return md
    else:
        # It's a thought to record — detect mood first
        thought_text = text
        mood = "neutral"
        try:
            from salmalm.features.mood import mood_detector

            mood, _ = mood_detector.detect(thought_text)
        except Exception:
            pass
        tid = thought_stream.add(thought_text, mood=mood)
        tags = ""
        import re as _re2

        found_tags = _re2.findall(r"#(\w+)", thought_text)
        if found_tags:
            tags = f" 🏷️ {', '.join('#' + t for t in found_tags)}"
        return f"💭 생각 #{tid} 기록됨{tags}"


def _cmd_export_fn(cmd, session, **_):
    """Handle /export [md|json|html] command."""
    from salmalm.core.export import export_session

    parts = cmd.strip().split()
    fmt = parts[1] if len(parts) > 1 else "md"
    result = export_session(session, fmt=fmt)
    if result.get("ok"):
        return (
            f"📤 **Conversation exported**\n"
            f"Format: {fmt.upper()}\n"
            f"File: `{result['filename']}`\n"
            f"Size: {result['size']:,} bytes\n"
            f"Path: `{result['path']}`"
        )
    return f"❌ Export failed: {result.get('error', 'unknown error')}"


# Public alias
_cmd_export = _cmd_export_fn

# Exact-match slash commands
_SLASH_COMMANDS = {
    "/clear": _cmd_clear,
    "/help": _cmd_help,
    "/status": _cmd_status,
    "/tools": _cmd_tools,
    "/uptime": _cmd_uptime,
    "/latency": _cmd_latency,
    "/health detail": _cmd_health_detail,
    "/health_detail": _cmd_health_detail,
    "/prune": _cmd_prune,
    "/usage daily": _cmd_usage_daily,
    "/usage monthly": _cmd_usage_monthly,
    "/bookmarks": _cmd_bookmarks,
    "/security": _cmd_security,
    "/soul": _cmd_soul,
    "/soul reset": _cmd_soul_reset,
    "/context": _cmd_context,
    "/context detail": _cmd_context,
    "/model": _cmd_model,
    "/models": _cmd_model,
}

# Also add /usage to prefix commands

# Prefix-match slash commands (checked with startswith)
_SLASH_PREFIX_COMMANDS = [
    ("/usage", _cmd_usage),
    ("/think ", _cmd_think),
    ("/plan ", _cmd_plan),
    ("/compare ", _cmd_compare),
    ("/model ", _cmd_model),
    ("/tts", _cmd_tts),
    ("/voice", _cmd_voice),
    ("/subagents", _cmd_subagents),
    ("/agent", _cmd_agent),
    ("/hooks", _cmd_hooks),
    ("/plugins", _cmd_plugins),
    ("/evolve", _cmd_evolve),
    ("/mood", _cmd_mood),
    ("/thought", _cmd_thought),
    ("/export", _cmd_export_fn),
]


async def _dispatch_slash_command(cmd, session, session_id, model_override, on_tool):
    """Dispatch slash commands. Returns response string or None if not a command."""
    # Exact match first
    handler = _SLASH_COMMANDS.get(cmd)
    if handler is not None:
        result = handler(cmd, session, session_id=session_id, model_override=model_override, on_tool=on_tool)
        if asyncio.iscoroutine(result):
            return await result
        return result

    # Prefix match
    for prefix, handler in _SLASH_PREFIX_COMMANDS:
        if cmd.startswith(prefix) or (not prefix.endswith(" ") and cmd == prefix.rstrip()):
            result = handler(cmd, session, session_id=session_id, model_override=model_override, on_tool=on_tool)
            if asyncio.iscoroutine(result):
                return await result
            return result

    return None
