"""CLI commands for sarathy."""

import asyncio
import os
import signal
from pathlib import Path
import select
import sys

import typer
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.table import Table
from rich.text import Text

from prompt_toolkit import PromptSession
from prompt_toolkit.formatted_text import HTML
from prompt_toolkit.history import FileHistory
from prompt_toolkit.patch_stdout import patch_stdout

from sarathy import __version__, __logo__
from sarathy.config.schema import Config

app = typer.Typer(
    name="sarathy",
    help=f"{__logo__} sarathy - Personal AI Assistant",
    no_args_is_help=True,
)

console = Console()
EXIT_COMMANDS = {"exit", "quit", "/exit", "/quit", ":q"}


def _require_config():
    """Check that config exists. If not, exit with onboarding message."""
    from sarathy.config.loader import get_config_path

    if not get_config_path().exists():
        console.print(f"[red]Config not found at ~/.sarathy/config.json[/red]")
        console.print("Please run [cyan]sarathy onboard[/cyan] first to set up sarathy.")
        raise typer.Exit(1)


# ---------------------------------------------------------------------------
# CLI input: prompt_toolkit for editing, paste, history, and display
# ---------------------------------------------------------------------------

_PROMPT_SESSION: PromptSession | None = None
_SAVED_TERM_ATTRS = None  # original termios settings, restored on exit


def _flush_pending_tty_input() -> None:
    """Drop unread keypresses typed while the model was generating output."""
    try:
        fd = sys.stdin.fileno()
        if not os.isatty(fd):
            return
    except Exception:
        return

    try:
        import termios

        termios.tcflush(fd, termios.TCIFLUSH)
        return
    except Exception:
        pass

    try:
        while True:
            ready, _, _ = select.select([fd], [], [], 0)
            if not ready:
                break
            if not os.read(fd, 4096):
                break
    except Exception:
        return


def _restore_terminal() -> None:
    """Restore terminal to its original state (echo, line buffering, etc.)."""
    if _SAVED_TERM_ATTRS is None:
        return
    try:
        import termios

        termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, _SAVED_TERM_ATTRS)
    except Exception:
        pass


def _init_prompt_session() -> None:
    """Create the prompt_toolkit session with persistent file history."""
    global _PROMPT_SESSION, _SAVED_TERM_ATTRS

    # Save terminal state so we can restore it on exit
    try:
        import termios

        _SAVED_TERM_ATTRS = termios.tcgetattr(sys.stdin.fileno())
    except Exception:
        pass

    history_file = Path.home() / ".sarathy" / "history" / "cli_history"
    history_file.parent.mkdir(parents=True, exist_ok=True)

    _PROMPT_SESSION = PromptSession(
        history=FileHistory(str(history_file)),
        enable_open_in_editor=False,
        multiline=False,  # Enter submits (single line mode)
    )


def _print_agent_response(response: str, render_markdown: bool) -> None:
    """Render assistant response with consistent terminal styling."""
    content = response or ""
    body = Markdown(content) if render_markdown else Text(content)
    console.print()
    console.print(f"[cyan]{__logo__} sarathy[/cyan]")
    console.print(body)
    console.print()


def _is_exit_command(command: str) -> bool:
    """Return True when input should end interactive chat."""
    return command.lower() in EXIT_COMMANDS


async def _read_interactive_input_async() -> str:
    """Read user input using prompt_toolkit (handles paste, history, display).

    prompt_toolkit natively handles:
    - Multiline paste (bracketed paste mode)
    - History navigation (up/down arrows)
    - Clean display (no ghost characters or artifacts)
    """
    if _PROMPT_SESSION is None:
        raise RuntimeError("Call _init_prompt_session() first")
    try:
        with patch_stdout():
            return await _PROMPT_SESSION.prompt_async(
                HTML("<b fg='ansiblue'>You:</b> "),
            )
    except EOFError as exc:
        raise KeyboardInterrupt from exc


def version_callback(value: bool):
    if value:
        console.print(f"{__logo__} sarathy v{__version__}")
        raise typer.Exit()


@app.callback()
def main(
    version: bool = typer.Option(None, "--version", "-v", callback=version_callback, is_eager=True),
):
    """sarathy - Personal AI Assistant."""
    pass


# ============================================================================
# Onboard / Setup
# ============================================================================


@app.command()
def onboard():
    """Interactive wizard to set up sarathy."""
    from sarathy.config.loader import get_config_path
    from sarathy.config.schema import Config
    from sarathy.utils.helpers import get_workspace_path

    config_path = get_config_path()
    config = Config()

    workspace = get_workspace_path()
    if not workspace.exists():
        workspace.mkdir(parents=True, exist_ok=True)

    _create_workspace_templates(workspace)
    _create_global_skills()

    from sarathy.cli.onboard import run_onboarding

    run_onboarding(config, config_path, workspace)


def _create_workspace_templates(workspace: Path):
    """Create default workspace template files from bundled templates."""
    from importlib.resources import files as pkg_files

    templates_dir = pkg_files("sarathy") / "templates"

    for item in templates_dir.iterdir():
        if not item.name.endswith(".md"):
            continue
        dest = workspace / item.name
        if not dest.exists():
            dest.write_text(item.read_text(encoding="utf-8"), encoding="utf-8")
            console.print(f"  [dim]Created {item.name}[/dim]")

    memory_dir = workspace / "memory"
    memory_dir.mkdir(exist_ok=True)

    memory_template = templates_dir / "memory" / "MEMORY.md"
    memory_file = memory_dir / "MEMORY.md"
    if not memory_file.exists():
        memory_file.write_text(memory_template.read_text(encoding="utf-8"), encoding="utf-8")
        console.print("  [dim]Created memory/MEMORY.md[/dim]")

    history_file = memory_dir / "HISTORY.md"
    if not history_file.exists():
        history_file.write_text("", encoding="utf-8")
        console.print("  [dim]Created memory/HISTORY.md[/dim]")

    # Create workspace skills directory and populate with starter skills
    workspace_skills_dir = workspace / "skills"
    workspace_skills_dir.mkdir(exist_ok=True)

    # Copy starter skills from templates
    template_skills_dir = templates_dir / "skills"
    if template_skills_dir.is_dir():
        for skill_dir in template_skills_dir.iterdir():
            if skill_dir.is_dir():
                dest_skill_dir = workspace_skills_dir / skill_dir.name
                dest_skill_dir.mkdir(exist_ok=True)
                skill_file = skill_dir / "SKILL.md"
                dest_skill_file = dest_skill_dir / "SKILL.md"
                if not dest_skill_file.exists() and skill_file.is_file():
                    dest_skill_file.write_text(
                        skill_file.read_text(encoding="utf-8"), encoding="utf-8"
                    )
                    console.print(f"  [dim]Created skills/{skill_dir.name}/SKILL.md[/dim]")


def _create_global_skills():
    """Create ~/.sarathy/skills/ with built-in skills from the package."""
    from importlib.resources import files as pkg_files

    global_skills_dir = Path.home() / ".sarathy" / "skills"
    global_skills_dir.mkdir(parents=True, exist_ok=True)

    # Get built-in skills from the package
    builtin_skills_dir = Path(__file__).parent.parent / "skills"

    if not builtin_skills_dir.exists():
        return

    # Copy built-in skills to ~/.sarathy/skills/ if they don't exist
    for skill_dir in builtin_skills_dir.iterdir():
        if skill_dir.is_dir():
            dest_skill_dir = global_skills_dir / skill_dir.name
            dest_skill_dir.mkdir(exist_ok=True)
            skill_file = skill_dir / "SKILL.md"
            dest_skill_file = dest_skill_dir / "SKILL.md"
            if not dest_skill_file.exists() and skill_file.is_file():
                dest_skill_file.write_text(skill_file.read_text(encoding="utf-8"), encoding="utf-8")
                console.print(f"  [dim]Copied built-in skill: {skill_dir.name}[/dim]")


def _make_provider(config: Config):
    """Create the appropriate LLM provider from config."""
    from sarathy.providers.litellm_provider import LiteLLMProvider
    from sarathy.providers.custom_provider import CustomProvider

    model = config.agents.defaults.model
    provider_name = config.get_provider_name()
    if not provider_name:
        console.print("[red]Error: No provider configured.[/red]")
        console.print("Set 'provider' in agents.defaults in ~/.sarathy/config.json")
        raise typer.Exit(1)
    p = config.get_provider()

    # Custom: direct OpenAI-compatible endpoint, bypasses LiteLLM
    if provider_name == "custom":
        return CustomProvider(
            api_key=p.api_key if p else "no-key",
            api_base=config.get_api_base() or "http://localhost:8000/v1",
            default_model=model,
        )

    from sarathy.providers.registry import find_by_name

    spec = find_by_name(provider_name)
    if spec and spec.is_local:
        pass
    elif (
        not model.startswith("bedrock/") and not (p and p.api_key) and not (spec and spec.is_oauth)
    ):
        console.print("[red]Error: No API key configured.[/red]")
        console.print("Set one in ~/.sarathy/config.json under providers section")
        raise typer.Exit(1)

    return LiteLLMProvider(
        api_key=p.api_key if p else None,
        api_base=config.get_api_base(),
        default_model=model,
        extra_headers=p.extra_headers if p else None,
        provider_name=provider_name,
    )


# ============================================================================
# Gateway / Server (start, stop, status, logs)
# ============================================================================

gateway_app = typer.Typer(help="Manage the sarathy gateway")
app.add_typer(gateway_app, name="gateway")


@gateway_app.command("start")
def gateway_start(
    port: int = typer.Option(18790, "--port", "-p", help="Gateway port"),
    verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
):
    """Start the sarathy gateway in the background."""
    _require_config()

    from sarathy.gateway.manager import get_log_file_path, is_gateway_running, start_gateway

    if is_gateway_running():
        console.print("[yellow]Gateway is already running.[/yellow]")
        raise typer.Exit(1)

    log_path = get_log_file_path()
    console.print(f"[dim]Starting gateway on port {port}...[/dim]")
    console.print(f"[dim]Logs: {log_path}[/dim]")

    try:
        start_gateway(port=port, verbose=verbose)
        console.print(f"[green]✓[/green] Gateway started (PID will be saved)")
    except RuntimeError as e:
        console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1)


@gateway_app.command("stop")
def gateway_stop():
    """Stop the running sarathy gateway."""
    from sarathy.gateway.manager import is_gateway_running, stop_gateway

    if not is_gateway_running():
        console.print("[yellow]Gateway is not running.[/yellow]")
        raise typer.Exit(1)

    if stop_gateway():
        console.print("[green]✓[/green] Gateway stopped")
    else:
        console.print("[red]Failed to stop gateway[/red]")
        raise typer.Exit(1)


@gateway_app.command("restart")
def gateway_restart(
    port: int = typer.Option(18790, "--port", "-p", help="Gateway port"),
    verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
):
    """Restart the sarathy gateway (stop and start)."""
    from sarathy.gateway.manager import (
        get_log_file_path,
        is_gateway_running,
        start_gateway,
        stop_gateway,
    )

    was_running = is_gateway_running()

    if was_running:
        console.print("[dim]Stopping gateway...[/dim]")
        if not stop_gateway():
            console.print("[red]Failed to stop gateway[/red]")
            raise typer.Exit(1)
        console.print("[green]✓[/green] Gateway stopped")

    console.print(f"[dim]Starting gateway on port {port}...[/dim]")
    console.print(f"[dim]Logs: {get_log_file_path()}[/dim]")

    try:
        start_gateway(port=port, verbose=verbose)
        console.print(f"[green]✓[/green] Gateway started")
    except RuntimeError as e:
        console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1)


@gateway_app.command("status")
def gateway_status():
    """Show gateway status."""
    from sarathy.gateway.manager import get_gateway_status

    status = get_gateway_status()

    if status["running"]:
        console.print(f"[green]✓[/green] Gateway is [bold]running[/bold]")
        console.print(f"  PID: {status['pid']}")
        console.print(f"  Log: {status['log_file']}")
    else:
        console.print("[dim]Gateway is [bold]not running[/bold]")


@gateway_app.command("logs")
def gateway_logs(
    lines: int = typer.Option(50, "--lines", "-n", help="Number of log lines to show"),
    follow: bool = typer.Option(False, "--follow", "-f", help="Follow log output (like tail -f)"),
):
    """Show gateway logs."""
    from sarathy.gateway.manager import get_recent_logs, get_log_file_path, is_gateway_running

    if not is_gateway_running():
        console.print("[yellow]Gateway is not running. No logs available.[/yellow]")
        raise typer.Exit(1)

    if follow:
        import subprocess
        import signal

        from sarathy.gateway.manager import get_latest_log_file

        log_file = get_latest_log_file()
        if not log_file or not log_file.exists():
            console.print("[red]No log file found.[/red]")
            raise typer.Exit(1)

        console.print(f"[dim]Following {log_file}... (Ctrl+C to exit)[/dim]")
        proc = None
        try:
            proc = subprocess.Popen(["tail", "-f", str(log_file)], stdout=1, stderr=1)
            signal.signal(signal.SIGINT, signal.SIG_IGN)
            proc.wait()
        except KeyboardInterrupt:
            if proc:
                proc.terminate()
        return

    logs = get_recent_logs(lines=lines)
    if logs:
        console.print(Panel(logs, title="Gateway Logs", border_style="dim"))
    else:
        console.print("[dim]No logs available.[/dim]")


# ============================================================================
# Agent Commands
# ============================================================================


@app.command()
def agent(
    message: str = typer.Option(None, "--message", "-m", help="Message to send to the agent"),
    session_id: str = typer.Option("cli:direct", "--session", "-s", help="Session ID"),
    markdown: bool = typer.Option(
        True, "--markdown/--no-markdown", help="Render assistant output as Markdown"
    ),
    logs: bool = typer.Option(
        False, "--logs/--no-logs", help="Show sarathy runtime logs during chat"
    ),
):
    """Interact with the agent directly."""
    from sarathy.config.loader import load_config, get_data_dir
    from sarathy.bus.queue import MessageBus
    from sarathy.agent.loop import AgentLoop
    from sarathy.cron.service import CronService
    from loguru import logger

    config = load_config()

    bus = MessageBus()
    provider = _make_provider(config)

    # Create cron service for tool usage (no callback needed for CLI unless running)
    cron_store_path = get_data_dir() / "cron" / "jobs.json"
    cron = CronService(cron_store_path)

    if logs:
        logger.enable("sarathy")
    else:
        logger.disable("sarathy")

    agent_loop = AgentLoop(
        bus=bus,
        provider=provider,
        workspace=config.workspace_path,
        model=config.agents.defaults.model,
        temperature=config.agents.defaults.temperature,
        max_tokens=config.agents.defaults.max_tokens,
        max_iterations=config.agents.defaults.max_tool_iterations,
        memory_window=config.agents.defaults.memory_window,
        web_search_config=config.tools.web.search,
        exec_config=config.tools.exec,
        cron_service=cron,
        restrict_to_workspace=config.tools.restrict_to_workspace,
        session_cache_size=config.agents.defaults.session_cache_size,
        max_session_messages=config.agents.defaults.max_session_messages,
        context_length=config.agents.defaults.context_length,
        mcp_servers=config.tools.mcp_servers,
        channels_config=config.channels,
        reasoning_effort=config.agents.defaults.reasoning_effort,
    )

    # Show spinner when logs are off (no output to miss); skip when logs are on
    def _thinking_ctx():
        if logs:
            from contextlib import nullcontext

            return nullcontext()
        # Animated spinner is safe to use with prompt_toolkit input handling
        return console.status("[dim]sarathy is thinking...[/dim]", spinner="dots")

    async def _cli_progress(content: str, *, tool_hint: bool = False) -> None:
        ch = agent_loop.channels_config
        if ch and tool_hint and not ch.send_tool_hints:
            return
        if ch and not tool_hint and not ch.send_progress:
            return
        console.print(f"  [dim]↳ {content}[/dim]")

    if message:
        # Single message mode — direct call, no bus needed
        async def run_once():
            with _thinking_ctx():
                response = await agent_loop.process_direct(
                    message, session_id, on_progress=_cli_progress
                )
            _print_agent_response(response, render_markdown=markdown)
            await agent_loop.close_mcp()

        asyncio.run(run_once())
    else:
        # Interactive mode — route through bus like other channels
        from sarathy.bus.events import InboundMessage

        _init_prompt_session()
        console.print(
            f"{__logo__} Interactive mode (type [bold]exit[/bold] or [bold]Ctrl+C[/bold] to quit)\n"
        )

        if ":" in session_id:
            cli_channel, cli_chat_id = session_id.split(":", 1)
        else:
            cli_channel, cli_chat_id = "cli", session_id

        def _exit_on_sigint(signum, frame):
            _restore_terminal()
            console.print("\nGoodbye!")
            os._exit(0)

        signal.signal(signal.SIGINT, _exit_on_sigint)

        async def run_interactive():
            bus_task = asyncio.create_task(agent_loop.run())
            turn_done = asyncio.Event()
            turn_done.set()
            turn_response: list[str] = []

            async def _consume_outbound():
                while True:
                    try:
                        msg = await asyncio.wait_for(bus.consume_outbound(), timeout=1.0)
                        if msg.metadata.get("_progress"):
                            is_tool_hint = msg.metadata.get("_tool_hint", False)
                            ch = agent_loop.channels_config
                            if ch and is_tool_hint and not ch.send_tool_hints:
                                pass
                            elif ch and not is_tool_hint and not ch.send_progress:
                                pass
                            else:
                                console.print(f"  [dim]↳ {msg.content}[/dim]")
                        elif not turn_done.is_set():
                            if msg.content:
                                turn_response.append(msg.content)
                            turn_done.set()
                        elif msg.content:
                            console.print()
                            _print_agent_response(msg.content, render_markdown=markdown)
                    except asyncio.TimeoutError:
                        continue
                    except asyncio.CancelledError:
                        break

            outbound_task = asyncio.create_task(_consume_outbound())

            try:
                while True:
                    try:
                        _flush_pending_tty_input()
                        user_input = await _read_interactive_input_async()
                        command = user_input.strip()
                        if not command:
                            continue

                        if _is_exit_command(command):
                            _restore_terminal()
                            console.print("\nGoodbye!")
                            break

                        turn_done.clear()
                        turn_response.clear()

                        await bus.publish_inbound(
                            InboundMessage(
                                channel=cli_channel,
                                sender_id="user",
                                chat_id=cli_chat_id,
                                content=user_input,
                            )
                        )

                        with _thinking_ctx():
                            await turn_done.wait()

                        if turn_response:
                            _print_agent_response(turn_response[0], render_markdown=markdown)
                    except KeyboardInterrupt:
                        _restore_terminal()
                        console.print("\nGoodbye!")
                        break
                    except EOFError:
                        _restore_terminal()
                        console.print("\nGoodbye!")
                        break
            finally:
                agent_loop.stop()
                outbound_task.cancel()
                await asyncio.gather(bus_task, outbound_task, return_exceptions=True)
                await agent_loop.close_mcp()

        asyncio.run(run_interactive())


# ============================================================================
# Channel Commands
# ============================================================================


channels_app = typer.Typer(help="Manage channels")
app.add_typer(channels_app, name="channels")


@channels_app.command("status")
def channels_status():
    """Show channel status."""
    from sarathy.config.loader import load_config

    config = load_config()

    table = Table(title="Channel Status")
    table.add_column("Channel", style="cyan")
    table.add_column("Enabled", style="green")
    table.add_column("Configuration", style="yellow")

    # Telegram
    tg = config.channels.telegram
    tg_config = f"token: {tg.token[:10]}..." if tg.token else "[dim]not configured[/dim]"
    table.add_row("Telegram", "✓" if tg.enabled else "✗", tg_config)

    # Discord
    dc = config.channels.discord
    table.add_row("Discord", "✓" if dc.enabled else "✗", dc.gateway_url)

    # Email
    em = config.channels.email
    em_config = em.imap_host if em.imap_host else "[dim]not configured[/dim]"
    table.add_row("Email", "✓" if em.enabled else "✗", em_config)

    console.print(table)


# ============================================================================
# Cron Commands
# ============================================================================

cron_app = typer.Typer(help="Manage scheduled tasks")
app.add_typer(cron_app, name="cron")


@cron_app.command("list")
def cron_list(
    all: bool = typer.Option(False, "--all", "-a", help="Include disabled jobs"),
):
    """List scheduled jobs."""
    from sarathy.config.loader import get_data_dir
    from sarathy.cron.service import CronService

    store_path = get_data_dir() / "cron" / "jobs.json"
    service = CronService(store_path)

    jobs = service.list_jobs(include_disabled=all)

    if not jobs:
        console.print("No scheduled jobs.")
        return

    table = Table(title="Scheduled Jobs")
    table.add_column("ID", style="cyan")
    table.add_column("Name")
    table.add_column("Schedule")
    table.add_column("Status")
    table.add_column("Next Run")

    import time
    from datetime import datetime as _dt
    from zoneinfo import ZoneInfo

    for job in jobs:
        # Format schedule
        if job.schedule.kind == "every":
            sched = f"every {(job.schedule.every_ms or 0) // 1000}s"
        elif job.schedule.kind == "cron":
            sched = (
                f"{job.schedule.expr or ''} ({job.schedule.tz})"
                if job.schedule.tz
                else (job.schedule.expr or "")
            )
        else:
            sched = "one-time"

        # Format next run
        next_run = ""
        if job.state.next_run_at_ms:
            ts = job.state.next_run_at_ms / 1000
            try:
                tz = ZoneInfo(job.schedule.tz) if job.schedule.tz else None
                next_run = _dt.fromtimestamp(ts, tz).strftime("%Y-%m-%d %H:%M")
            except Exception:
                next_run = time.strftime("%Y-%m-%d %H:%M", time.localtime(ts))

        status = "[green]enabled[/green]" if job.enabled else "[dim]disabled[/dim]"

        table.add_row(job.id, job.name, sched, status, next_run)

    console.print(table)


@cron_app.command("add")
def cron_add(
    name: str = typer.Option(..., "--name", "-n", help="Job name"),
    message: str = typer.Option(..., "--message", "-m", help="Message for agent"),
    every: int = typer.Option(None, "--every", "-e", help="Run every N seconds"),
    cron_expr: str = typer.Option(None, "--cron", "-c", help="Cron expression (e.g. '0 9 * * *')"),
    tz: str | None = typer.Option(
        None, "--tz", help="IANA timezone for cron (e.g. 'America/Vancouver')"
    ),
    at: str = typer.Option(None, "--at", help="Run once at time (ISO format)"),
    deliver: bool = typer.Option(False, "--deliver", "-d", help="Deliver response to channel"),
    to: str = typer.Option(None, "--to", help="Recipient for delivery"),
    channel: str = typer.Option(
        None, "--channel", help="Channel for delivery (e.g. 'telegram', 'discord', 'email')"
    ),
):
    """Add a scheduled job."""
    from sarathy.config.loader import get_data_dir
    from sarathy.cron.service import CronService
    from sarathy.cron.types import CronSchedule

    if tz and not cron_expr:
        console.print("[red]Error: --tz can only be used with --cron[/red]")
        raise typer.Exit(1)

    # Determine schedule type
    if every:
        schedule = CronSchedule(kind="every", every_ms=every * 1000)
    elif cron_expr:
        schedule = CronSchedule(kind="cron", expr=cron_expr, tz=tz)
    elif at:
        import datetime

        dt = datetime.datetime.fromisoformat(at)
        schedule = CronSchedule(kind="at", at_ms=int(dt.timestamp() * 1000))
    else:
        console.print("[red]Error: Must specify --every, --cron, or --at[/red]")
        raise typer.Exit(1)

    store_path = get_data_dir() / "cron" / "jobs.json"
    service = CronService(store_path)

    try:
        job = service.add_job(
            name=name,
            schedule=schedule,
            message=message,
            deliver=deliver,
            to=to,
            channel=channel,
        )
    except ValueError as e:
        console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1) from e

    console.print(f"[green]✓[/green] Added job '{job.name}' ({job.id})")


@cron_app.command("remove")
def cron_remove(
    job_id: str = typer.Argument(..., help="Job ID to remove"),
):
    """Remove a scheduled job."""
    from sarathy.config.loader import get_data_dir
    from sarathy.cron.service import CronService

    store_path = get_data_dir() / "cron" / "jobs.json"
    service = CronService(store_path)

    if service.remove_job(job_id):
        console.print(f"[green]✓[/green] Removed job {job_id}")
    else:
        console.print(f"[red]Job {job_id} not found[/red]")


@cron_app.command("enable")
def cron_enable(
    job_id: str = typer.Argument(..., help="Job ID"),
    disable: bool = typer.Option(False, "--disable", help="Disable instead of enable"),
):
    """Enable or disable a job."""
    from sarathy.config.loader import get_data_dir
    from sarathy.cron.service import CronService

    store_path = get_data_dir() / "cron" / "jobs.json"
    service = CronService(store_path)

    job = service.enable_job(job_id, enabled=not disable)
    if job:
        status = "disabled" if disable else "enabled"
        console.print(f"[green]✓[/green] Job '{job.name}' {status}")
    else:
        console.print(f"[red]Job {job_id} not found[/red]")


@cron_app.command("run")
def cron_run(
    job_id: str = typer.Argument(..., help="Job ID to run"),
    force: bool = typer.Option(False, "--force", "-f", help="Run even if disabled"),
):
    """Manually run a job."""
    from loguru import logger
    from sarathy.config.loader import load_config, get_data_dir
    from sarathy.cron.service import CronService
    from sarathy.cron.types import CronJob
    from sarathy.bus.queue import MessageBus
    from sarathy.agent.loop import AgentLoop

    logger.disable("sarathy")

    config = load_config()
    provider = _make_provider(config)
    bus = MessageBus()
    agent_loop = AgentLoop(
        bus=bus,
        provider=provider,
        workspace=config.workspace_path,
        model=config.agents.defaults.model,
        temperature=config.agents.defaults.temperature,
        max_tokens=config.agents.defaults.max_tokens,
        max_iterations=config.agents.defaults.max_tool_iterations,
        memory_window=config.agents.defaults.memory_window,
        web_search_config=config.tools.web.search,
        exec_config=config.tools.exec,
        restrict_to_workspace=config.tools.restrict_to_workspace,
        session_cache_size=config.agents.defaults.session_cache_size,
        max_session_messages=config.agents.defaults.max_session_messages,
        context_length=config.agents.defaults.context_length,
        mcp_servers=config.tools.mcp_servers,
        channels_config=config.channels,
        reasoning_effort=config.agents.defaults.reasoning_effort,
    )

    store_path = get_data_dir() / "cron" / "jobs.json"
    service = CronService(store_path)

    result_holder = []

    async def on_job(job: CronJob) -> str | None:
        response = await agent_loop.process_direct(
            job.payload.message,
            session_key=f"cron:{job.id}",
            channel=job.payload.channel or "cli",
            chat_id=job.payload.to or "direct",
        )
        result_holder.append(response)
        return response

    service.on_job = on_job

    async def run():
        return await service.run_job(job_id, force=force)

    if asyncio.run(run()):
        console.print("[green]✓[/green] Job executed")
        if result_holder:
            _print_agent_response(result_holder[0], render_markdown=True)
    else:
        console.print(f"[red]Failed to run job {job_id}[/red]")


# ============================================================================
# Status Commands
# ============================================================================


@app.command()
def status():
    """Show sarathy status."""
    from sarathy.config.loader import load_config, get_config_path

    config_path = get_config_path()
    config = load_config()
    workspace = config.workspace_path

    console.print(f"{__logo__} sarathy Status\n")

    console.print(
        f"Config: {config_path} {'[green]✓[/green]' if config_path.exists() else '[red]✗[/red]'}"
    )
    console.print(
        f"Workspace: {workspace} {'[green]✓[/green]' if workspace.exists() else '[red]✗[/red]'}"
    )

    if config_path.exists():
        from sarathy.providers.registry import PROVIDERS

        console.print(f"Model: {config.agents.defaults.model}")

        # Show explicit provider from config
        provider_name = config.agents.defaults.provider
        console.print(f"Provider: {provider_name}")

        # Check API keys from registry
        for spec in PROVIDERS:
            p = getattr(config.providers, spec.name, None)
            if p is None:
                continue
            if spec.is_oauth:
                console.print(f"{spec.label}: [green]✓ (OAuth)[/green]")
            elif spec.is_local:
                # Local deployments show api_base instead of api_key
                if p.api_base:
                    console.print(f"{spec.label}: [green]✓ {p.api_base}[/green]")
                else:
                    console.print(f"{spec.label}: [dim]not set[/dim]")
            else:
                has_key = bool(p.api_key)
                console.print(
                    f"{spec.label}: {'[green]✓[/green]' if has_key else '[dim]not set[/dim]'}"
                )

        # Web search status
        ws = config.tools.web.search
        if ws.enabled:
            env_key = "FIRECRAWL_API_KEY" if ws.provider == "firecrawl" else "BRAVE_API_KEY"
            has_key = bool(ws.api_key or os.environ.get(env_key))
            console.print(
                f"Web Search ({ws.provider}): {'[green]✓[/green]' if has_key else '[yellow]⚠ no API key[/yellow]'}"
            )
        else:
            console.print("Web Search: [dim]disabled[/dim]")


# ============================================================================
# Provider Management
# ============================================================================
# Provider Management
# ============================================================================

provider_app = typer.Typer(help="Manage providers")
app.add_typer(provider_app, name="provider")


@provider_app.command("login")
def provider_login(
    provider: str = typer.Argument(..., help="Provider name (e.g. 'ollama', 'lmstudio', 'vllm')"),
):
    """Authenticate with a provider (not needed for local providers)."""
    from sarathy.providers.registry import PROVIDERS

    key = provider.replace("-", "_")
    spec = next((s for s in PROVIDERS if s.name == key), None)
    if not spec:
        names = ", ".join(s.name for s in PROVIDERS)
        console.print(f"[red]Unknown provider: {provider}[/red]  Available: {names}")
        raise typer.Exit(1)

    if spec.is_local:
        console.print(
            f"[green]✓ {spec.label} is a local provider - no authentication needed.[/green]"
        )
        console.print(
            f"  Make sure {spec.label} is running and accessible at the configured endpoint."
        )
    else:
        console.print(f"[yellow]For {spec.label}, set API key in config:[/yellow]")
        console.print(f"  providers.{spec.name}.apiKey = 'your-api-key'")


if __name__ == "__main__":
    app()
