"""Built-in slash commands for Navi Code TUI."""

from __future__ import annotations

import datetime
import os
import subprocess
import sys

from clients.terminal import api
from clients.terminal.config import settings
from clients.terminal.tui.commands.base import BaseCommand, CommandMeta
from clients.terminal.tui.context import TuiContext
from clients.terminal.tui.settings import get_tui_settings


class HelpCommand(BaseCommand):
    meta = CommandMeta(
        name="help",
        aliases=(),
        description="Show available slash commands.",
        keybind=None,
    )

    async def execute(self, ctx: TuiContext, args: str) -> None:
        from clients.terminal.tui.commands.registry import get_registry

        registry = get_registry()
        # Plain section headers (no rich markup): this content is delivered as a
        # ``status`` event, and ``StatusRenderer`` builds a literal ``Text(...)``
        # which does not parse markup — ``[b]…[/b]`` would show as literal square
        # brackets instead of bold.
        lines = ["Slash commands"]
        for cmd in registry.all():
            aliases = f" ({', '.join(cmd.meta.aliases)})" if cmd.meta.aliases else ""
            key = f" [{cmd.meta.keybind}]" if cmd.meta.keybind else ""
            lines.append(f"  /{cmd.meta.name}{aliases}{key} — {cmd.meta.description}")

        # Keys (not slash commands) so they're discoverable from /help too.
        lines.append("")
        lines.append("Keys")
        lines.append("  Ctrl+P — command palette")
        lines.append("  Ctrl+R — reading mode (toggle)")
        lines.append("  Ctrl+X Q — quit")
        lines.append("  Ctrl+X C — compact context")
        lines.append("  Ctrl+X T — toggle thinking blocks")
        lines.append("  Esc — stop generation")
        lines.append("  Ctrl+Shift+Up/Down, Ctrl+End — scroll chat")

        # Copying text out of the TUI — the non-obvious bit worth surfacing.
        lines.append("")
        lines.append("Copying text")
        lines.append(
            "  Drag (no Shift) + Ctrl+C — copy a message/output (clean, no borders)."
        )
        lines.append(
            "  Works via OSC 52; in terminals without it (e.g. GNOME Console) use:"
        )
        lines.append(
            "  Ctrl+R → Shift+drag → Ctrl+Shift+C — copy raw selection to the system"
        )
        lines.append(
            "  clipboard. Reading mode hides the chrome and the bubble borders so the"
        )
        lines.append("  selection is just the conversation text.")
        ctx.chat_panel.handle_ws_event({"type": "status", "content": "\n".join(lines)})


class NewCommand(BaseCommand):
    meta = CommandMeta(
        name="new",
        aliases=("clear",),
        description="Start a new session.",
        keybind=None,
    )

    async def execute(self, ctx: TuiContext, args: str) -> None:
        app = ctx.app()
        if app is None:
            return
        app.run_worker(app._create_new_session_worker())


class SessionsCommand(BaseCommand):
    meta = CommandMeta(
        name="sessions",
        aliases=("resume", "continue"),
        description="Pick a navi_code session to switch to.",
        keybind=None,
    )

    async def execute(self, ctx: TuiContext, args: str) -> None:
        app = ctx.app()
        if app is None:
            return
        app._open_sessions_picker()


class SwitchCommand(BaseCommand):
    meta = CommandMeta(
        name="switch",
        aliases=(),
        description="Switch to a navi_code session by id/prefix, or open the picker.",
        keybind=None,
    )

    async def execute(self, ctx: TuiContext, args: str) -> None:
        app = ctx.app()
        if app is None:
            return

        target = args.strip()
        # No argument (or none/ambiguous match) -> open the picker, pre-filtered
        # to whatever the user typed so they can finish the choice visually.
        if not target:
            app._open_sessions_picker()
            return

        session = self._resolve_session(target)
        if session is None:
            app._open_sessions_picker(initial_query=target)
            return

        app.run_worker(app._switch_session_worker(session["session_id"]))

    @staticmethod
    def _resolve_session(target: str) -> dict | None:
        """Resolve ``target`` to a single navi_code session, or None.

        Tries an exact id first (GET /sessions/{id}); if that fails or lands on a
        different profile, falls back to prefix-matching the navi_code session
        list. Returns the session only when exactly one navi_code match exists.
        """
        # Exact id hit.
        try:
            session = api.get_session(target)
        except Exception:
            session = None
        if session is not None and session.get("profile_id") == settings.default_profile_id:
            return session

        # Prefix match among navi_code sessions.
        try:
            sessions = api.list_sessions()
        except Exception:
            return None
        matches = [
            s
            for s in sessions
            if s.get("profile_id") == settings.default_profile_id
            and s.get("session_id", "").startswith(target)
        ]
        if len(matches) == 1:
            return matches[0]
        return None


class ProfileCommand(BaseCommand):
    meta = CommandMeta(
        name="profile",
        aliases=(),
        description="Show current session profile.",
        keybind=None,
    )

    async def execute(self, ctx: TuiContext, args: str) -> None:
        if not ctx.session_id:
            ctx.chat_panel.handle_ws_event({"type": "error", "message": "No active session"})
            return
        try:
            session = api.get_session(ctx.session_id)
        except Exception as exc:
            ctx.chat_panel.handle_ws_event(
                {"type": "error", "message": f"Failed to get session: {exc}"}
            )
            return
        ctx.chat_panel.handle_ws_event(
            {
                "type": "status",
                "content": f"Profile: {session.get('profile_id')}\nSession: {session.get('session_id')}",
            }
        )


class QuitCommand(BaseCommand):
    meta = CommandMeta(
        name="quit",
        aliases=("exit", "q"),
        description="Exit Navi Code.",
        keybind="ctrl+x q",
    )

    async def execute(self, ctx: TuiContext, args: str) -> None:
        app = ctx.app()
        app.exit()


class ThinkingCommand(BaseCommand):
    meta = CommandMeta(
        name="thinking",
        aliases=(),
        description="Toggle thinking block visibility.",
        keybind=None,
    )

    async def execute(self, ctx: TuiContext, args: str) -> None:
        settings.show_thinking = not settings.show_thinking
        ctx.chat_panel.handle_ws_event(
            {
                "type": "status",
                "content": f"Thinking blocks: {'on' if settings.show_thinking else 'off'}",
            }
        )


class CompactCommand(BaseCommand):
    meta = CommandMeta(
        name="compact",
        aliases=(),
        description="Compact the current session (force context compression now).",
        keybind="ctrl+x c",
    )

    async def execute(self, ctx: TuiContext, args: str) -> None:
        # Send a typed control message, not a chat message: the backend runs the
        # real context compressor (bypassing the token threshold) instead of a
        # full agent turn that merely produces summary text.
        if ctx.ws_client:
            ctx.ws_client.enqueue({"type": "compact"})


class ThemesCommand(BaseCommand):
    meta = CommandMeta(
        name="themes",
        aliases=(),
        description="Open the theme picker.",
        keybind=None,
    )

    async def execute(self, ctx: TuiContext, args: str) -> None:
        app = ctx.app()
        current = getattr(app, "_theme_name", "gnexus-dark")

        def on_picked(theme_name: str | None) -> None:
            if theme_name is None:
                ctx.chat_panel.handle_ws_event(
                    {"type": "status", "content": "Theme selection cancelled"}
                )
                return
            app = ctx.app()
            app._theme_name = theme_name
            app.apply_theme()
            tui_settings = ctx.settings or get_tui_settings()
            tui_settings.theme = theme_name
            tui_settings.save()
            ctx.chat_panel.handle_ws_event(
                {"type": "status", "content": f"Theme set to {theme_name}"}
            )

        from clients.terminal.tui.screens.theme_picker import ThemePickerScreen

        app.push_screen(ThemePickerScreen(current), callback=on_picked)


class MouseCommand(BaseCommand):
    meta = CommandMeta(
        name="mouse",
        aliases=(),
        description="Toggle mouse support in the TUI (requires restart).",
        keybind=None,
    )

    async def execute(self, ctx: TuiContext, args: str) -> None:
        tui_settings = ctx.settings or get_tui_settings()
        if args.strip().lower() in ("on", "true", "1", "yes"):
            tui_settings.mouse = True
        elif args.strip().lower() in ("off", "false", "0", "no"):
            tui_settings.mouse = False
        else:
            tui_settings.mouse = not tui_settings.mouse
        tui_settings.save()
        state = "on" if tui_settings.mouse else "off"
        ctx.chat_panel.handle_ws_event(
            {
                "type": "status",
                "content": f"Mouse support set to {state}. Restart navi-code to apply.",
            }
        )


class ExportCommand(BaseCommand):
    meta = CommandMeta(
        name="export",
        aliases=("save",),
        description="Export current session to markdown and open $EDITOR.",
        keybind=None,
    )

    async def execute(self, ctx: TuiContext, args: str) -> None:
        if not ctx.session_id:
            ctx.chat_panel.handle_ws_event(
                {"type": "error", "message": "No active session to export"}
            )
            return
        try:
            session = api.get_session(ctx.session_id)
        except Exception as exc:
            ctx.chat_panel.handle_ws_event(
                {"type": "error", "message": f"Failed to get session: {exc}"}
            )
            return

        short_id = ctx.session_id[:8]
        timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"{short_id}_{timestamp}.md"
        exports_dir = ctx.state.state_dir / "exports"
        exports_dir.mkdir(parents=True, exist_ok=True)
        file_path = exports_dir / filename

        try:
            file_path.write_text(_render_export_markdown(session), encoding="utf-8")
        except OSError as exc:
            ctx.chat_panel.handle_ws_event(
                {"type": "error", "message": f"Failed to write export: {exc}"}
            )
            return

        try:
            _open_in_editor(str(file_path))
        except OSError as exc:
            ctx.chat_panel.handle_ws_event(
                {
                    "type": "error",
                    "message": f"Failed to open editor for {file_path}: {exc}",
                }
            )
            return
        ctx.chat_panel.handle_ws_event({"type": "status", "content": f"Exported to {file_path}"})


def _render_export_markdown(session: dict) -> str:
    lines: list[str] = []
    session_id = session.get("session_id", "unknown")
    profile_id = session.get("profile_id", "unknown")
    created = session.get("created_at", "")
    lines.append(f"# Navi Code Export — {session_id[:8]}")
    lines.append("")
    lines.append(f"- **Profile:** {profile_id}")
    lines.append(f"- **Session:** {session_id}")
    if created:
        lines.append(f"- **Created:** {created}")
    lines.append("")

    messages = session.get("messages", [])
    for msg in messages:
        role = msg.get("role", "unknown")
        content = msg.get("content", "")
        if not content:
            continue
        heading = role.capitalize()
        lines.append(f"## {heading}")
        lines.append("")
        lines.append(content)
        lines.append("")

    return "\n".join(lines)


def _open_in_editor(path: str) -> None:
    editor = os.environ.get("EDITOR")
    if not editor:
        editor = "notepad" if sys.platform == "win32" else "vi"
    # Detach so the TUI is not blocked while the editor runs.
    try:
        subprocess.Popen([editor, path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    except OSError as exc:
        raise OSError(f"cannot start editor '{editor}': {exc}") from exc
