"""Command handlers for all slash commands."""

from __future__ import annotations

import asyncio
import logging
from pathlib import Path
from typing import TYPE_CHECKING

from sygen_bot.cli.auth import check_all_auth
from sygen_bot.i18n import t
from sygen_bot.infra.version import check_pypi, get_current_version
from sygen_bot.orchestrator.registry import OrchestratorResult
from sygen_bot.orchestrator.selectors.cron_selector import cron_selector_start
from sygen_bot.orchestrator.selectors.model_selector import model_selector_start, switch_model
from sygen_bot.orchestrator.selectors.models import Button, ButtonGrid
from sygen_bot.orchestrator.selectors.session_selector import session_selector_start
from sygen_bot.orchestrator.selectors.task_selector import task_selector_start
from sygen_bot.text.response_format import SEP, fmt, new_session_text
from sygen_bot.workspace.loader import clear_cron_results, read_mainmemory

if TYPE_CHECKING:
    from sygen_bot.orchestrator.core import Orchestrator
    from sygen_bot.session.key import SessionKey

logger = logging.getLogger(__name__)


# -- Command wrappers (registered by Orchestrator._register_commands) --


async def cmd_reset(orch: Orchestrator, key: SessionKey, _text: str) -> OrchestratorResult:
    """Handle /new: kill processes and reset only active provider session."""
    logger.info("Reset requested")
    await orch._process_registry.kill_all(key.chat_id)
    clear_cron_results(orch.paths)
    provider = await orch.reset_active_provider_session(key)
    return OrchestratorResult(text=new_session_text(provider))


async def cmd_status(orch: Orchestrator, key: SessionKey, _text: str) -> OrchestratorResult:
    """Handle /status."""
    logger.info("Status requested")
    return OrchestratorResult(text=await _build_status(orch, key))


async def cmd_model(orch: Orchestrator, key: SessionKey, text: str) -> OrchestratorResult:
    """Handle /model [name]."""
    logger.info("Model requested")
    parts = text.split(None, 1)
    if len(parts) < 2:
        resp = await model_selector_start(orch, key)
        return OrchestratorResult(text=resp.text, buttons=resp.buttons)
    name = parts[1].strip()
    result_text = await switch_model(orch, key, name)
    return OrchestratorResult(text=result_text)


async def cmd_memory(orch: Orchestrator, _key: SessionKey, _text: str) -> OrchestratorResult:
    """Handle /memory."""
    logger.info("Memory requested")
    content = await asyncio.to_thread(read_mainmemory, orch.paths)
    if not content.strip():
        return OrchestratorResult(
            text=fmt(
                t("memory.header"),
                SEP,
                t("memory.empty"),
                SEP,
                t("memory.empty_tip"),
            ),
        )
    return OrchestratorResult(
        text=fmt(
            t("memory.header"),
            SEP,
            content,
            SEP,
            t("memory.filled_tip"),
        ),
    )


async def cmd_sessions(orch: Orchestrator, key: SessionKey, _text: str) -> OrchestratorResult:
    """Handle /sessions."""
    logger.info("Sessions requested")
    resp = await session_selector_start(orch, key.chat_id)
    return OrchestratorResult(text=resp.text, buttons=resp.buttons)


async def cmd_tasks(orch: Orchestrator, key: SessionKey, _text: str) -> OrchestratorResult:
    """Handle /tasks."""
    logger.info("Tasks requested")
    hub = orch.task_hub
    if hub is None:
        return OrchestratorResult(
            text=fmt(t("tasks.header"), SEP, t("tasks.disabled")),
        )
    resp = task_selector_start(hub, key.chat_id)
    return OrchestratorResult(text=resp.text, buttons=resp.buttons)


async def cmd_cron(orch: Orchestrator, _key: SessionKey, _text: str) -> OrchestratorResult:
    """Handle /cron."""
    logger.info("Cron requested")
    resp = await cron_selector_start(orch)
    return OrchestratorResult(text=resp.text, buttons=resp.buttons)


async def cmd_logs(orch: Orchestrator, _key: SessionKey, text: str) -> OrchestratorResult:
    """Handle /logs [subcommand]."""
    from datetime import datetime, timedelta, timezone

    from sygen_bot.observability.recorder import Trace, read_traces

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

    kwargs: dict[str, object] = {"limit": 10}
    header = "📋 Recent traces"

    if sub == "cron":
        kwargs["trace_type"] = "cron"
        header = "📋 Recent cron traces"
    elif sub == "tasks":
        kwargs["trace_type"] = "task"
        header = "📋 Recent task traces"
    elif sub == "errors":
        kwargs["errors_only"] = True
        kwargs["since"] = datetime.now(timezone.utc) - timedelta(hours=24)
        header = "⚠️ Errors (last 24h)"
    elif sub:
        kwargs["name"] = sub
        header = f"📋 Traces: {sub}"

    traces: list[Trace] = await asyncio.to_thread(
        read_traces, orch.paths.logs_dir, **kwargs  # type: ignore[arg-type]
    )

    if not traces:
        return OrchestratorResult(text=f"{header}\n\nNo traces found.")

    lines = [header, ""]
    for tr in traces:
        icon = {"ok": "✅", "error": "❌", "timeout": "⏱", "aborted": "🚫"}.get(tr.status, "❓")
        lines.append(f"{icon} **{tr.name}**")
        lines.append(f"   {tr.type} · {tr.status} · {tr.duration_sec}s · {tr.provider}/{tr.model}")
        lines.append(f"   {tr.started}")
        if tr.error and sub == "errors":
            lines.append(f"   ⚠️ {tr.error}")
        if tr.summary:
            lines.append(f"   → {tr.summary[:100]}")
        lines.append("")

    return OrchestratorResult(text="\n".join(lines))


async def cmd_upgrade(orch: Orchestrator, key: SessionKey, _text: str) -> OrchestratorResult:
    """Handle /upgrade: upgrade sygen package and show changelog."""
    logger.info("Upgrade requested")
    from sygen_bot.infra.install import detect_install_mode, is_upgradeable
    from sygen_bot.infra.updater import perform_upgrade_pipeline, write_upgrade_sentinel
    from sygen_bot.infra.version import fetch_changelog

    current = get_current_version()

    if not is_upgradeable():
        # Dev/editable install — fall back to git pull
        import subprocess

        sygen_dir = Path.home() / "Agents" / "sygen"
        if not (sygen_dir / ".git").is_dir():
            return OrchestratorResult(text="\u274c Dev install, but git repo not found.")
        try:
            result = subprocess.run(
                ["git", "-C", str(sygen_dir), "pull", "--ff-only", "origin", "main"],
                capture_output=True,
                text=True,
                timeout=30,
            )
            output = result.stdout.strip()
            if "Already up to date" in output:
                return OrchestratorResult(text="\u2705 Already up to date.")
            return OrchestratorResult(
                text=fmt(
                    "\u2705 Updated:",
                    SEP,
                    f"```\n{output}\n```",
                    "\nRestart to apply: /restart",
                ),
            )
        except Exception as e:
            return OrchestratorResult(text=f"\u274c Update failed: {e}")

    # pip/pipx install — use the upgrade pipeline
    info = await check_pypi()
    if info and not info.update_available:
        return OrchestratorResult(text=f"\u2705 Already on latest version: `{current}`")

    changed, installed_version, output = await perform_upgrade_pipeline(
        current_version=current,
    )

    if not changed:
        tail = output[-400:] if output else ""
        details = f"\n```\n{tail}\n```" if tail else ""
        return OrchestratorResult(
            text=f"\u274c Upgrade failed (still on `{current}`){details}",
        )

    # Fetch changelog for the new version
    changelog = await fetch_changelog(installed_version)
    changelog_block = f"\n\n{changelog}" if changelog else ""

    # Write sentinel for post-restart notification
    await asyncio.to_thread(
        write_upgrade_sentinel,
        orch.paths.sygen_home,
        chat_id=key.chat_id,
        old_version=current,
        new_version=installed_version,
    )

    return OrchestratorResult(
        text=fmt(
            f"\u2705 Updated: `{current}` → `{installed_version}`",
            SEP,
            changelog_block,
            f"\nRestart to apply: /restart",
        ),
    )


def _build_codex_cache_block(orch: Orchestrator) -> str:
    """Build the Codex model cache section for /diagnose."""
    if not orch._observers.codex_cache_obs:
        return "\n🔄 " + t("diagnose.codex_cache_not_init")
    cache = orch._observers.codex_cache_obs.get_cache()
    if not cache or not cache.models:
        return "\n🔄 " + t("diagnose.codex_cache_not_loaded")
    default_model = next((m.id for m in cache.models if m.is_default), "N/A")
    return "\n🔄 " + t(
        "diagnose.codex_cache_info",
        updated=cache.last_updated,
        count=len(cache.models),
        default=default_model,
    )


def _build_diagnose_health_block(orch: Orchestrator) -> str:
    """Build the multi-agent health section for /diagnose."""
    supervisor = orch._supervisor
    if supervisor is None:
        return ""
    status_icon = {"running": "●", "starting": "◐", "crashed": "✖", "stopped": "○"}
    agent_lines = ["\n" + t("diagnose.health_header")]
    for name in sorted(supervisor.health.keys()):
        h = supervisor.health[name]
        icon = status_icon.get(h.status, "?")
        role = "main" if name == "main" else "sub"
        line = f"  {icon} `{name}` [{role}] — {h.status}"
        if h.status == "running" and h.uptime_human:
            line += f" ({h.uptime_human})"
        if h.restart_count > 0:
            line += f" | restarts: {h.restart_count}"
        if h.status == "crashed" and h.last_crash_error:
            line += f"\n      `{h.last_crash_error[:100]}`"
        agent_lines.append(line)
    return "\n".join(agent_lines)


def _resolve_log_path(orch: Orchestrator) -> Path:
    """Return the best available log file path.

    Sub-agents don't have their own log files — fall back to the central
    log in the main sygen home (parent of ``agents/<name>``).
    """
    log_path = orch.paths.logs_dir / "agent.log"
    if not log_path.exists():
        main_logs = orch.paths.sygen_home.parent.parent / "logs" / "agent.log"
        if main_logs.exists():
            return main_logs
    return log_path


async def cmd_diagnose(orch: Orchestrator, _key: SessionKey, _text: str) -> OrchestratorResult:
    """Handle /diagnose."""
    logger.info("Diagnose requested")
    version = get_current_version()
    effective_model, effective_provider = orch.resolve_runtime_target(orch._config.model)
    info_block = (
        f"{t('diagnose.version_line', version=version)}\n"
        f"{t('diagnose.configured_line', provider=orch._config.provider, model=orch._config.model)}\n"
        f"{t('diagnose.effective_line', provider=effective_provider, model=effective_model)}"
    )

    cache_block = _build_codex_cache_block(orch)
    agent_block = _build_diagnose_health_block(orch)

    log_tail = await _read_log_tail(_resolve_log_path(orch))
    log_block = (
        f"{t('diagnose.log_header')}\n```\n{log_tail}\n```" if log_tail else t("diagnose.no_log")
    )

    return OrchestratorResult(
        text=fmt(t("diagnose.header"), SEP, info_block, cache_block, agent_block, SEP, log_block),
    )


# -- Helpers ------------------------------------------------------------------


def _build_agent_health_block(orch: Orchestrator) -> str:
    """Build the multi-agent health section for /status (main agent only)."""
    supervisor = orch._supervisor
    if supervisor is None or len(supervisor.health) <= 1:
        return ""

    status_icon = {
        "running": "●",
        "starting": "◐",
        "crashed": "✖",
        "stopped": "○",
    }
    agent_lines = [t("status.agents_header")]
    for name in sorted(supervisor.health.keys()):
        if name == "main":
            continue
        h = supervisor.health[name]
        icon = status_icon.get(h.status, "?")
        line = f"  {icon} {name} — {h.status}"
        if h.status == "running" and h.uptime_human:
            line += f" ({h.uptime_human})"
        if h.restart_count > 0:
            line += f" ⟳{h.restart_count}"
        if h.status == "crashed" and h.last_crash_error:
            line += f"\n      {h.last_crash_error[:80]}"
        agent_lines.append(line)
    return "\n".join(agent_lines)


async def _build_status(orch: Orchestrator, key: SessionKey) -> str:
    """Build the /status response text."""
    runtime_model, _runtime_provider = orch.resolve_runtime_target(orch._config.model)
    configured_model = orch._config.model

    def _model_line(model_name: str) -> str:
        if model_name == configured_model:
            return t("status.model_line", model=model_name)
        return t("status.model_line_configured", model=model_name, configured=configured_model)

    session = await orch._sessions.get_active(key)
    if session:
        topic_line = (
            f"{t('status.topic_line', topic=session.topic_name)}\n" if session.topic_name else ""
        )
        session_block = (
            f"{topic_line}"
            f"{t('status.session_line', sid=session.session_id[:8] + '...')}\n"
            f"{t('status.messages_line', count=session.message_count)}\n"
            f"{t('status.tokens_line', tokens=f'{session.total_tokens:,}')}\n"
            f"{t('status.cost_line', cost=f'{session.total_cost_usd:.4f}')}\n"
            f"{_model_line(session.model)}"
        )
    else:
        session_block = f"{t('status.no_session')}\n{_model_line(runtime_model)}"

    bg_tasks = orch.active_background_tasks(key.chat_id)
    bg_block = ""
    if bg_tasks:
        import time

        bg_lines = [t("status.bg_header", count=len(bg_tasks))]
        for bg_t in bg_tasks:
            age = time.monotonic() - bg_t.submitted_at
            bg_lines.append(f"  `{bg_t.task_id}` {bg_t.prompt[:40]}... ({age:.0f}s)")
        bg_block = "\n".join(bg_lines)

    auth = await asyncio.to_thread(check_all_auth)
    auth_lines: list[str] = []
    for provider, result in auth.items():
        age_label = f" ({result.age_human})" if result.age_human else ""
        auth_lines.append(f"  [{provider}] {result.status.value}{age_label}")
    auth_block = t("status.auth_header") + "\n" + "\n".join(auth_lines)

    agent_block = _build_agent_health_block(orch)

    blocks = [t("status.header"), SEP, session_block]
    if bg_block:
        blocks += [SEP, bg_block]
    blocks += [SEP, auth_block]
    if agent_block:
        blocks += [SEP, agent_block]
    return fmt(*blocks)


async def _read_log_tail(log_path: Path, lines: int = 50) -> str:
    """Read the last *lines* of a log file without blocking the event loop."""

    def _read() -> str:
        if not log_path.is_file():
            return ""
        try:
            text = log_path.read_text(encoding="utf-8", errors="replace")
            return "\n".join(text.strip().splitlines()[-lines:])
        except OSError:
            return "(could not read log file)"

    return await asyncio.to_thread(_read)
