"""TUI command routing and CLI launch integration."""

from __future__ import annotations

import contextlib
import json
from collections import defaultdict
from collections.abc import Callable
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any

from cleveragents.application.container import get_container
from cleveragents.tui.app import (
    _FALLBACK_SESSION_ID,
    _PRE_SESSION_PERSONA_KEY,
    CleverAgentsTuiApp,
    textual_available,
)
from cleveragents.tui.persona.registry import PersonaRegistry
from cleveragents.tui.persona.state import PersonaState
from cleveragents.tui.slash_catalog import SLASH_COMMAND_SPECS

if TYPE_CHECKING:
    from cleveragents.a2a.facade import A2aLocalFacade
    from cleveragents.application.services.session_service import SessionService

# Module-level singleton for the TUI session service — mirrors the CLI pattern
# in cli/commands/session.py (_get_session_service).  Both _build_tui_facade()
# and _create_tui_session() must share the same SessionService instance so they
# use the same SQLAlchemy engine and sessionmaker rather than creating two
# separate engines against the same SQLite file (providers.Factory creates a
# new instance on every call).
_tui_session_service: SessionService | None = None


def _get_tui_session_service() -> SessionService | None:
    """Return the process-wide TUI SessionService, creating it on first call.

    Returns ``None`` if the session service cannot be initialised (e.g.
    missing database URL), so callers degrade gracefully.
    """
    global _tui_session_service
    if _tui_session_service is None:
        with contextlib.suppress(
            RuntimeError, ValueError, ImportError, AttributeError, OSError
        ):
            _tui_session_service = get_container().session_service()
    return _tui_session_service


@dataclass(slots=True)
class TuiCommandRouter:
    """Slash command router used by the TUI prompt.

    Parameters
    ----------
    persona_registry:
        Registry of available personas.
    persona_state:
        Mutable state tracking the active persona per session.
    container_factory:
        Optional callable that returns the DI container.  Defaults to
        :func:`get_container` when *None*.  Accepting this parameter
        enables constructor-based dependency injection, which is
        essential for test isolation under ``multiprocessing.fork()``
        where ``unittest.mock.patch`` context managers do not propagate
        to child workers.
    """

    persona_registry: PersonaRegistry
    persona_state: PersonaState
    container_factory: Callable[[], Any] | None = field(default=None, repr=False)

    def _resolve_container(self) -> Any:
        """Return the DI container, using the injected factory or the global default."""
        if self.container_factory is not None:
            return self.container_factory()
        return get_container()

    def handle(self, raw: str, *, session_id: str) -> str:
        tokens = raw.strip().split()
        if not tokens:
            return "Empty command"
        if tokens[0] == "persona":
            return self._persona_command(tokens[1:], session_id=session_id)
        if tokens[0] == "session":
            return self._session_command(tokens[1:], session_id=session_id)
        if tokens[0] == "settings":
            return self._settings_command(tokens[1:])
        if tokens[0] == "help":
            return self._help_command(tokens[1:])
        return f"Unknown command: /{raw}"

    def _settings_command(self, tokens: list[str]) -> str:
        """Handle /settings command.

        Opens the settings screen for TUI configuration.
        """
        return "Settings screen opened"

    def _help_command(self, tokens: list[str]) -> str:
        """Handle /help [command].

        With no arguments, lists all catalogued slash commands grouped by
        namespace.  With a command argument (e.g. ``/help persona:list``),
        renders command-specific help (description, usage, aliases).
        """
        if not tokens:
            return self._help_list_all()
        return self._help_for_command(tokens[0])

    def _help_list_all(self) -> str:
        """Return a formatted listing of all commands grouped by namespace."""
        groups: dict[str, list[str]] = defaultdict(list)
        for spec in SLASH_COMMAND_SPECS:
            groups[spec.group].append(f"  /{spec.command} — {spec.description}")
        lines: list[str] = ["Available slash commands:"]
        for group in sorted(groups):
            lines.append(f"\n{group}:")
            lines.extend(groups[group])
        return "\n".join(lines)

    def _help_for_command(self, command: str) -> str:
        """Return help text for a specific command, or a not-found message."""
        # Strip leading slash if the user typed e.g. /help /persona:list
        cmd = command.lstrip("/")
        for spec in SLASH_COMMAND_SPECS:
            if spec.command == cmd:
                return (
                    f"/{spec.command}\n"
                    f"  Group:       {spec.group}\n"
                    f"  Description: {spec.description}"
                )
        return f"Unknown command: /{cmd}"

    def _persona_command(self, tokens: list[str], *, session_id: str) -> str:
        if not tokens or tokens[0] == "list":
            names = [persona.name for persona in self.persona_registry.list_personas()]
            return ", ".join(names) if names else "No personas"
        if tokens[0] == "set":
            if len(tokens) < 2:
                return "Usage: /persona set <name>"
            persona = self.persona_state.set_active_persona(session_id, tokens[1])
            return f"Active persona: {persona.name}"
        return f"Unknown persona command: {' '.join(tokens)}"

    def _session_command(self, tokens: list[str], *, session_id: str) -> str:
        if not tokens or tokens[0] == "show":
            return f"Current session: {session_id}"
        if tokens[0] == "export":
            return self._session_export(tokens[1:], session_id=session_id)
        if tokens[0] == "import":
            return self._session_import(tokens[1:])
        return f"Unknown session command: {' '.join(tokens)}"

    def _session_export(self, tokens: list[str], *, session_id: str) -> str:
        """Handle /session:export [--format md] [path].

        Exports the current session to JSON (default) or Markdown.
        If a path is provided the file is written there; otherwise a
        confirmation message is returned for display in the TUI.
        """
        fmt = "json"
        path: str | None = None

        # Parse optional --format flag and optional path
        i = 0
        while i < len(tokens):
            if tokens[i] == "--format" and i + 1 < len(tokens):
                fmt = tokens[i + 1]
                i += 2
            else:
                path = tokens[i]
                i += 1

        if fmt not in ("json", "md", "txt"):
            return f"Invalid format: {fmt!r}. Use 'json', 'md', or 'txt'."

        try:
            container = self._resolve_container()
            service = container.session_service()
            export_data = service.export_session(session_id)

            if fmt == "md":
                from cleveragents.domain.models.core.session import (
                    MessageRole,
                    SessionMessage,
                )

                session = service.get(session_id)
                messages = [
                    SessionMessage(
                        message_id=m["message_id"],
                        role=MessageRole(m["role"]),
                        content=m["content"],
                        sequence=m["sequence"],
                        timestamp=m["timestamp"],
                        metadata=m.get("metadata", {}),
                        tool_call_id=m.get("tool_call_id"),
                    )
                    for m in export_data.get("messages", [])
                ]
                session.messages = messages
                content = session.as_export_markdown()
            elif fmt == "txt":
                from cleveragents.domain.models.core.session import (
                    MessageRole,
                    SessionMessage,
                )

                session = service.get(session_id)
                messages = [
                    SessionMessage(
                        message_id=m["message_id"],
                        role=MessageRole(m["role"]),
                        content=m["content"],
                        sequence=m["sequence"],
                        timestamp=m["timestamp"],
                        metadata=m.get("metadata", {}),
                        tool_call_id=m.get("tool_call_id"),
                    )
                    for m in export_data.get("messages", [])
                ]
                session.messages = messages
                content = session.as_export_plain_text()
            else:
                content = json.dumps(export_data, indent=2, default=str)

            if path is not None:
                out = Path(path)
                out.parent.mkdir(parents=True, exist_ok=True)
                out.write_text(content, encoding="utf-8")
                return f"Session exported to {path}"
            return f"Session exported ({fmt.upper()})"

        except Exception as exc:
            return f"Export failed: {exc}"

    def _session_import(self, tokens: list[str]) -> str:
        """Handle /session:import <path>.

        Imports a session from a JSON file previously produced by
        ``/session:export`` or ``agents session export``.
        """
        if not tokens:
            return "Usage: /session:import <path>"

        path = Path(tokens[0])
        if not path.exists():
            return f"File not found: {path}"

        try:
            raw = path.read_text(encoding="utf-8")
            data = json.loads(raw)
        except json.JSONDecodeError as exc:
            return f"Invalid JSON: {exc}"

        try:
            container = self._resolve_container()
            service = container.session_service()
            session = service.import_session(data)
            return f"Session imported: {session.session_id}"
        except Exception as exc:
            return f"Import failed: {exc}"


def _build_tui_facade() -> A2aLocalFacade | None:
    """Build and wire an :class:`~cleveragents.a2a.facade.A2aLocalFacade` for the TUI.

    Follows the same pattern as ``_facade_dispatch`` in
    ``cli/commands/session.py``: the facade is retrieved via
    :func:`~cleveragents.a2a.cli_bootstrap.get_facade` and the session
    workflow is registered so ``message/send`` resolves through the real
    LLM stack.  Any failure is suppressed so the TUI degrades gracefully
    to preview-only mode rather than crashing.
    """

    try:
        from cleveragents.a2a.cli_bootstrap import get_facade
        from cleveragents.application.services.session_workflow import SessionWorkflow

        facade = get_facade()
        session_svc = _get_tui_session_service()
        if session_svc is not None:
            facade.register_service("session_service", session_svc)

        try:
            provider_registry = None
            try:
                from cleveragents.providers.registry import get_provider_registry

                provider_registry = get_provider_registry()
            except (ImportError, RuntimeError, OSError):
                pass
            if session_svc is not None:
                facade.register_service(
                    "session_workflow",
                    SessionWorkflow(
                        session_service=session_svc,
                        provider_registry=provider_registry,
                    ),
                )
        except (RuntimeError, ValueError, ImportError, AttributeError, OSError):
            pass
        return facade
    except (ImportError, RuntimeError, AttributeError, OSError):  # pragma: no cover
        return None


def _create_tui_session(persona_state: PersonaState | None = None) -> str:
    """Create a real database-backed TUI session and return its session_id.

    When *persona_state* is provided, the active persona's actor is bound to
    the session at creation time so the first message dispatches to the LLM
    without hitting ``SessionActorNotConfiguredError``.

    Falls back to ``_FALLBACK_SESSION_ID`` if the session service is
    unavailable so the TUI can still launch without a database.
    """
    actor_name: str | None = None
    if persona_state is not None:
        with contextlib.suppress(ValueError, AttributeError, KeyError):
            actor_name = (
                persona_state.active_persona(_PRE_SESSION_PERSONA_KEY).actor or None
            )

    service = _get_tui_session_service()
    if service is None:
        return _FALLBACK_SESSION_ID
    try:
        session = service.create(actor_name=actor_name)
        return session.session_id
    except (RuntimeError, ValueError, AttributeError, OSError):
        return _FALLBACK_SESSION_ID


def run_tui(*, headless: bool = False) -> int:
    """Run the Textual TUI app or a headless startup check."""
    container = get_container()
    registry = container.persona_registry()
    state = container.persona_state(registry=registry)
    router = TuiCommandRouter(persona_registry=registry, persona_state=state)

    if headless:
        payload = {
            "textual_available": textual_available(),
            "persona_count": len(registry.list_personas())
            if registry.personas_dir.exists()
            else 0,
            "default_persona": state.active_name(_PRE_SESSION_PERSONA_KEY),
            "help": router.handle("help", session_id=_PRE_SESSION_PERSONA_KEY),
        }
        print(json.dumps(payload, indent=2))
        return 0

    facade = _build_tui_facade()
    session_id = _create_tui_session(persona_state=state)

    app = CleverAgentsTuiApp(
        command_router=router,
        persona_state=state,
        facade=facade,
        session_id=session_id,
    )
    app.run()
    return 0
