"""Slash command implementations for neut chat.

Each command is a standalone function for testability.
Commands return a string to display, or None for no output.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from axiom.setup.renderer import _c, _Colors

if TYPE_CHECKING:
    from axiom.infra.orchestrator.session import Session, SessionStore

    from .agent import ChatAgent


def cmd_help() -> str:
    """Return the help text, auto-synced from CLI registry."""
    lines = [
        "",
        f"  {_c(_Colors.BOLD, 'Chat Commands:')}",
        f"  {_c(_Colors.CYAN, '/help')}                   Show this help",
        f"  {_c(_Colors.CYAN, '/status')}                 Session info, gateway, usage",
        f"  {_c(_Colors.CYAN, '/usage')}                  Token usage and cost breakdown",
        f"  {_c(_Colors.CYAN, '/sessions')}               Browse and manage sessions",
        f"  {_c(_Colors.CYAN, '/sessions rename')} <title>  Rename current session",
        f"  {_c(_Colors.CYAN, '/sessions archive')} [id|#]  Archive session(s)",
        f"  {_c(_Colors.CYAN, '/resume')} <id|#>          Load a session by ID or number",
        f"  {_c(_Colors.CYAN, '/new')}                    Start a fresh session",
        f"  {_c(_Colors.CYAN, '/clear')}                  Clear message history",
        f"  {_c(_Colors.CYAN, '/compact')}                Summarize conversation to save tokens",
        f"  {_c(_Colors.CYAN, '/model')}                  Switch LLM provider mid-chat",
        f"  {_c(_Colors.CYAN, '/context')}                Show context window usage",
        f"  {_c(_Colors.CYAN, '/save')}                   Save last response to knowledge corpus",
        f"  {_c(_Colors.CYAN, '/doctor')}                 Quick health check",
        f"  {_c(_Colors.CYAN, '/update')}                 Check for and apply updates",
        f"  {_c(_Colors.CYAN, '/exit')}                   Save and exit",
        "",
        f"  {_c(_Colors.BOLD, 'Signal:')}",
        f"  {_c(_Colors.CYAN, '/signal brief')}           Catch up on what happened",
        f"  {_c(_Colors.CYAN, '/signal status')}          Pipeline health",
        "",
        f"  {_c(_Colors.BOLD, 'Publisher:')}",
        f"  {_c(_Colors.CYAN, '/pub status')}             Document status",
        f"  {_c(_Colors.CYAN, '/pub overview')}           Document ecosystem dashboard",
        "",
    ]

    lines.extend(
        [
            f"  {_c(_Colors.DIM, 'Tip: Alt+Enter for newline in multi-line input.')}",
            f"  {_c(_Colors.DIM, 'CLI commands run directly:')} {_c(_Colors.CYAN, '/signal status')}",
            "",
        ]
    )
    return "\n".join(lines)


def cmd_status(agent: ChatAgent) -> str:
    """Return session status info with routing visibility."""
    session = agent.session
    gateway = agent.gateway
    provider = gateway.active_provider

    title_display = session.title or "(untitled)"
    lines = [
        "",
        f"  {_c(_Colors.BOLD, 'Session:')}  {session.session_id} — {title_display}",
        f"  {_c(_Colors.BOLD, 'Messages:')} {len(session.messages)}",
    ]

    # --- LLM Provider & Routing ---
    if provider:
        model_name = getattr(gateway, "_model_override", None) or provider.model
        lines.append(f"  {_c(_Colors.BOLD, 'Provider:')} {provider.name}")
        lines.append(f"  {_c(_Colors.BOLD, 'Model:')}    {model_name}")

        # Routing tier
        tier = getattr(provider, "routing_tier", "any")
        tier_labels = {
            "public": "public (cloud)",
            "export_controlled": "export-controlled (VPN)",
            "any": "unrestricted",
        }
        lines.append(f"  {_c(_Colors.BOLD, 'Tier:')}     {tier_labels.get(tier, tier)}")

        # Why this provider was selected
        reasons = []
        if getattr(gateway, "_provider_override", None):
            reasons.append(f"user override (--provider {gateway._provider_override})")
        elif getattr(gateway, "_model_override", None):
            reasons.append(f"model override (--model {gateway._model_override})")
        else:
            reasons.append(f"priority {provider.priority}")
        if getattr(provider, "requires_vpn", False):
            vpn_ok = gateway._check_vpn(provider)
            reasons.append(f"VPN {'reachable' if vpn_ok else 'UNREACHABLE'}")
        session_mode = getattr(agent, "_session_mode", "auto")
        if session_mode != "auto":
            reasons.append(f"session mode: {session_mode}")
        lines.append(f"  {_c(_Colors.BOLD, 'Reason:')}   {'; '.join(reasons)}")

        # Fallback chain
        fallback_names = []
        for p in sorted(gateway.providers, key=lambda p: p.priority):
            if p.name != provider.name and (p.api_key or not p.api_key_env):
                vpn_tag = " [vpn]" if getattr(p, "requires_vpn", False) else ""
                fallback_names.append(f"{p.name} ({p.model}){vpn_tag}")
        if fallback_names:
            lines.append(f"  {_c(_Colors.BOLD, 'Fallback:')} {' -> '.join(fallback_names)}")
        else:
            lines.append(f"  {_c(_Colors.BOLD, 'Fallback:')} {_c(_Colors.DIM, 'none')}")

        # Provider health — last call latency and error rate from usage tracker
        if agent.usage.turns:
            last = agent.usage.turns[-1]
            model_label = last.model or "unknown"
            lines.append(
                f"  {_c(_Colors.BOLD, 'Last call:')} "
                f"{last.input_tokens + last.output_tokens} tokens, model={model_label}"
            )
    else:
        lines.append(f"  {_c(_Colors.BOLD, 'Gateway:')}  stub mode (no LLM configured)")

    # Usage summary
    usage = agent.usage
    if usage.turns:
        lines.append(
            f"  {_c(_Colors.BOLD, 'Tokens:')}   "
            f"{usage.total_input_tokens}in / {usage.total_output_tokens}out "
            f"({usage.turn_count} turns)"
        )
        if usage.total_cost > 0:
            lines.append(f"  {_c(_Colors.BOLD, 'Cost:')}     ${usage.total_cost:.4f}")

    if session.context:
        ctx_keys = list(session.context.keys())
        lines.append(f"  {_c(_Colors.BOLD, 'Context:')}  {ctx_keys}")
    lines.append("")
    return "\n".join(lines)


def cmd_usage(agent: ChatAgent) -> str:
    """Return detailed token usage and cost breakdown."""
    usage = agent.usage
    if not usage.turns:
        return "\n  No usage data yet.\n"

    lines = [
        "",
        f"  {_c(_Colors.BOLD, 'Token Usage:')}",
        f"  Total input:  {usage.total_input_tokens:,}",
        f"  Total output: {usage.total_output_tokens:,}",
        f"  Total cost:   ${usage.total_cost:.4f}",
        f"  Turns:        {usage.turn_count}",
        "",
        f"  {_c(_Colors.BOLD, 'Per-Turn Breakdown:')}",
    ]
    for i, turn in enumerate(usage.turns, 1):
        model_label = turn.model or "unknown"
        cost_label = f"${turn.cost:.4f}" if turn.cost > 0 else "-"
        lines.append(
            f"  {i:3d}. {model_label:30s}  "
            f"{turn.input_tokens:>6d}in  {turn.output_tokens:>6d}out  {cost_label}"
        )
    lines.append("")
    return "\n".join(lines)


def cmd_clear(agent: ChatAgent) -> str:
    """Clear message history, keep session open."""
    agent.session.messages.clear()
    return f"\n  {_c(_Colors.DIM, 'Message history cleared.')}\n"


def cmd_compact(agent: ChatAgent) -> str:
    """Summarize conversation to save tokens (stub)."""
    return f"\n  {_c(_Colors.DIM, 'Compact not yet implemented — use /new to start fresh.')}\n"


def cmd_model(agent: ChatAgent) -> str:
    """Show current model and available providers."""
    gateway = agent.gateway
    lines = ["", f"  {_c(_Colors.BOLD, 'Available providers:')}"]
    for p in gateway.providers:
        marker = " *" if p == gateway.active_provider else ""
        lines.append(f"  {p.name} ({p.model}){marker}")
    lines.append("")
    lines.append(f"  {_c(_Colors.DIM, 'Switch with: /model <provider-name>')}")
    lines.append("")
    return "\n".join(lines)


def cmd_model_switch(agent: ChatAgent, provider_name: str) -> str:
    """Switch LLM provider mid-chat."""
    gateway = agent.gateway
    for p in gateway.providers:
        if p.name.lower() == provider_name.lower():
            gateway.set_provider_override(p.name)
            return f"\n  Switched to {_c(_Colors.CYAN, p.name)} ({p.model})\n"
    return f"\n  {_c(_Colors.RED, 'Provider not found:')} {provider_name}\n"


def cmd_context(agent: ChatAgent) -> str:
    """Show context window usage."""
    messages = agent.session.messages
    # Rough estimate: ~4 chars per token
    total_chars = sum(len(getattr(m, "content", "") or "") for m in messages)
    est_tokens = total_chars // 4
    # Most models use ~128k-200k context; show against 128k as baseline
    budget = 128_000
    pct = min(100.0, (est_tokens / budget) * 100) if budget else 0
    lines = [
        "",
        f"  {_c(_Colors.BOLD, 'Context Window:')}",
        f"  Messages:        {len(messages)}",
        f"  Est. tokens:     ~{est_tokens:,}",
        f"  Budget (approx): {budget:,}",
        f"  Usage:           {pct:.1f}%",
        "",
    ]
    return "\n".join(lines)


def cmd_doctor(agent: ChatAgent) -> str:
    """Quick health check: gateway, DB, extensions."""
    lines = ["", f"  {_c(_Colors.BOLD, 'Health Check:')}"]

    # Gateway
    gw = agent.gateway
    if gw.available:
        provider = gw.active_provider
        lines.append(f"  Gateway:    OK ({provider.name if provider else 'no provider'})")
    else:
        lines.append(f"  Gateway:    {_c(_Colors.RED, 'UNAVAILABLE')}")

    # DB connectivity
    try:
        from axiom.infra.orchestrator.session import SessionStore

        s = SessionStore()
        count = len(s.list_sessions())
        lines.append(f"  Sessions:   OK ({count} saved)")
    except Exception as e:
        lines.append(f"  Sessions:   {_c(_Colors.RED, f'ERROR: {e}')}")

    # Extensions
    try:
        from axiom.extensions.registry import get_registry

        reg = get_registry()
        ext_count = len(reg.extensions) if hasattr(reg, "extensions") else "?"
        lines.append(f"  Extensions: {ext_count} loaded")
    except Exception:
        lines.append(f"  Extensions: {_c(_Colors.DIM, 'unknown')}")

    lines.append("")
    return "\n".join(lines)


def cmd_signal() -> str:
    """Return signal pipeline status."""
    from .tools import execute_tool

    result = execute_tool("signal_status", {})
    lines = [""]
    lines.append(f"  {_c(_Colors.BOLD, 'Neut Signal Status')}")
    inbox = result.get("inbox_raw", {})
    if inbox:
        for source, count in inbox.items():
            lines.append(f"  inbox/{source}: {count} files")
    else:
        lines.append("  inbox: empty")
    lines.append(f"  processed: {result.get('processed', 0)}")
    lines.append(f"  drafts: {result.get('drafts', 0)}")
    lines.append("")
    return "\n".join(lines)


def cmd_doc() -> str:
    """Return document status."""
    from .tools import execute_tool

    result = execute_tool("query_docs", {})
    lines = [""]
    lines.append(f"  {_c(_Colors.BOLD, 'Document Status')}")
    docs = result.get("documents", [])
    if not docs:
        lines.append("  No tracked documents.")
    else:
        for d in docs:
            status = d.get("status", "unknown")
            version = d.get("version", "")
            lines.append(f"  {d['doc_id']}: {status} ({version})")
    lines.append("")
    return "\n".join(lines)


def cmd_sessions(store: SessionStore, input_prov=None) -> str:
    """Return formatted list of sessions with titles.

    If input_prov supports interactive selection (PTK), offers arrow-key
    session picking. Otherwise falls back to a numbered list.
    """
    session_ids = store.list_sessions()
    if not session_ids:
        return "\n  No saved sessions.\n"

    lines = ["", f"  {_c(_Colors.BOLD, 'Saved sessions:')}"]
    for i, sid in enumerate(session_ids[:15], 1):
        meta = store.load_meta(sid)
        if meta:
            title = meta.get("title") or _c(_Colors.DIM, "(untitled)")
            msg_count = meta["message_count"]
            updated = meta["updated_at"][:10] if meta["updated_at"] else ""
            idx = _c(_Colors.DIM, f"{i:2d}.")
            lines.append(
                f"  {idx} {_c(_Colors.CYAN, sid)}  "
                f"{title}  "
                f"{_c(_Colors.DIM, f'{msg_count} msgs  {updated}')}"
            )
        else:
            lines.append(f"  {_c(_Colors.DIM, f'{i:2d}.')} {_c(_Colors.CYAN, sid)}")
    if len(session_ids) > 15:
        lines.append(f"  {_c(_Colors.DIM, f'... and {len(session_ids) - 15} more')}")
    lines.extend(
        [
            "",
            f"  {_c(_Colors.DIM, 'Use')} {_c(_Colors.CYAN, '/resume <id>')} "
            f"{_c(_Colors.DIM, 'or')} {_c(_Colors.CYAN, '/resume <number>')} "
            f"{_c(_Colors.DIM, 'to load a session.')}",
            "",
        ]
    )
    return "\n".join(lines)


def cmd_resume(
    session_id: str,
    store: SessionStore,
    agent: ChatAgent,
) -> str:
    """Resume a session by ID or number (from /sessions list)."""
    # Support numeric index from /sessions list
    if session_id.isdigit():
        idx = int(session_id) - 1
        all_ids = store.list_sessions()
        if 0 <= idx < len(all_ids):
            session_id = all_ids[idx]
        else:
            return f"\n  {_c(_Colors.RED, 'Invalid session number:')} {session_id}\n"

    session = store.load(session_id)
    if session is None:
        return f"\n  {_c(_Colors.RED, 'Session not found:')} {session_id}\n"

    agent.session = session
    title_str = f" — {session.title}" if session.title else ""
    return (
        f"\n  Resumed session {_c(_Colors.CYAN, session_id)}{title_str} "
        f"({len(session.messages)} messages)\n"
    )


def cmd_new(store: SessionStore, agent: ChatAgent) -> str:
    """Start a fresh session. Returns status message."""
    # Save current session first
    store.save(agent.session)
    old_id = agent.session.session_id

    # Create new session
    new_session = store.create()
    agent.session = new_session

    return (
        f"\n  Saved {_c(_Colors.DIM, old_id)}, started {_c(_Colors.CYAN, new_session.session_id)}\n"
    )


def cmd_rename(agent: ChatAgent, store: SessionStore, title: str) -> str:
    """Rename the current session."""
    if not title:
        return "\n  Usage: /rename <title>\n"
    agent.session.title = title
    store.save(agent.session)
    return f"\n  Session renamed to: {_c(_Colors.CYAN, title)}\n"


def cmd_archive(
    session_id: str,
    store: SessionStore,
    agent: ChatAgent,
) -> str:
    """Archive a session (or the current one if no ID given)."""
    if not session_id:
        # Archive current session and start a new one
        target_id = agent.session.session_id
        store.save(agent.session)
        if store.archive(target_id):
            new_session = store.create()
            agent.session = new_session
            return (
                f"\n  Archived {_c(_Colors.DIM, target_id)}, "
                f"started {_c(_Colors.CYAN, new_session.session_id)}\n"
            )
        return f"\n  {_c(_Colors.RED, 'Failed to archive session')}\n"

    # Support numeric index
    if session_id.isdigit():
        idx = int(session_id) - 1
        all_ids = store.list_sessions()
        if 0 <= idx < len(all_ids):
            session_id = all_ids[idx]
        else:
            return f"\n  {_c(_Colors.RED, 'Invalid session number:')} {session_id}\n"

    if session_id == agent.session.session_id:
        return cmd_archive("", store, agent)

    if store.archive(session_id):
        return f"\n  Archived session {_c(_Colors.DIM, session_id)}\n"
    return f"\n  {_c(_Colors.RED, 'Session not found:')} {session_id}\n"


def cmd_save(session: Session, agent: ChatAgent | None = None, **kwargs) -> str:
    """Save the last assistant response as a knowledge fact in the local corpus."""
    if not session or not session.messages:
        return "\n  Nothing to save — no conversation yet.\n"

    # Find last assistant message
    last_assistant = None
    for msg in reversed(session.messages):
        if msg.role == "assistant":
            content = msg.content
            if isinstance(content, str) and content.strip():
                last_assistant = content
                break

    if not last_assistant:
        return "\n  No assistant response to save.\n"

    # Extract a summary (first paragraph or first 500 chars)
    summary = last_assistant.split("\n\n")[0][:500]

    # Save to knowledge events log
    try:
        from axiom.federation.knowledge_metrics import KnowledgeMetricsService

        svc = KnowledgeMetricsService()
        session_id = getattr(session, "session_id", getattr(session, "id", "unknown"))
        svc.record_event(
            "fact_added",
            fact_id=f"chat-{str(session_id)[:8]}-{len(session.messages)}",
            source="chat_save",
            domain="user_knowledge",
            maturity=1,
            content=summary,
        )

        # Also save to personal RAG if available
        try:
            from axiom.rag.store import RAGStore

            store = RAGStore()
            store.add_chunk(
                text=last_assistant,
                source=f"chat:{session_id}",
                corpus="rag-internal",
                metadata={"type": "saved_fact", "session": str(session_id)},
            )
        except Exception:
            pass  # RAG store not available — that's OK

        truncated = summary[:80]
        return (
            f'\n  Saved to local knowledge corpus.\n  "{truncated}..."\n  Maturity: 1 (personal)\n'
        )
    except Exception as e:
        return f"\n  Could not save: {e}\n"


# ---------------------------------------------------------------------------
# Dispatch table (auto-synced from CLI registry)
# ---------------------------------------------------------------------------

# Chat-specific meta commands
CHAT_META_COMMANDS = {
    "/help": "Show available commands",
    "/status": "Session info, gateway, usage",
    "/usage": "Token usage and cost breakdown",
    "/sessions": "Browse and manage sessions",
    "/sessions rename": "Rename current session (/sessions rename <title>)",
    "/sessions archive": "Archive session(s) (/sessions archive [id|#])",
    "/resume": "Load a session by ID or number (/resume <id|#>)",
    "/new": "Start a fresh session",
    "/clear": "Clear message history",
    "/compact": "Summarize conversation to save tokens",
    "/model": "Switch LLM provider mid-chat",
    "/context": "Show context window usage",
    "/save": "Save last assistant response to knowledge corpus",
    "/doctor": "Quick health check",
    "/update": "Check for and apply updates (/update [now|later|check])",
    "/exit": "Save and exit",
}


def _get_cli_commands() -> dict[str, str]:
    """Dynamically load CLI commands from registry.

    Returns commands keyed under their *visible* names (/signal, /pub).
    Hidden aliases (/sense, /doc) still work at dispatch time but are
    not included here so they stay out of /help and tab-completion.
    """
    cli_commands = {}

    # Import CLI modules and get their COMMANDS
    try:
        from axiom.extensions.builtins.eve_agent.cli import COMMANDS as sense_commands

        for name, help_text in sense_commands.items():
            cli_commands[f"/signal {name}"] = help_text
    except ImportError:
        pass

    try:
        from axiom.extensions.builtins.prt_agent.cli import COMMANDS as doc_commands

        for name, help_text in doc_commands.items():
            cli_commands[f"/pub {name}"] = help_text
    except ImportError:
        pass

    return cli_commands


def get_slash_commands() -> dict[str, str]:
    """Get all slash commands (meta + CLI).

    This is the single source of truth for available slash commands.
    CLI commands are auto-synced from their respective modules.
    """
    commands = CHAT_META_COMMANDS.copy()
    commands.update(_get_cli_commands())
    return commands


def find_close_command(cmd: str) -> str | None:
    """Return the closest matching slash command, or None.

    Uses difflib fuzzy matching on the first word of each command.
    Shared by both the classic REPL and the fullscreen TUI.
    """
    from difflib import get_close_matches

    all_commands = list(get_slash_commands().keys())
    first_words = [c.split()[0] for c in all_commands]
    matches = get_close_matches(
        cmd.split()[0],
        first_words,
        n=1,
        cutoff=0.5,
    )
    return matches[0] if matches else None


# For backwards compatibility
SLASH_COMMANDS = CHAT_META_COMMANDS.copy()  # Static fallback
