"""CLI entry point for yaacli.

Minimal CLI that launches the TUI application.
Most interactions happen inside the TUI via slash commands.
"""

from __future__ import annotations

import asyncio
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
from datetime import UTC, datetime
from importlib import resources
from pathlib import Path

import click
from dotenv import load_dotenv

from yaacli import __version__  # pyright: ignore[reportAttributeAccessIssue]
from yaacli.config import ConfigManager, WorktreeMetadata, YaacliConfig
from yaacli.logging import LOG_FILE_NAME, configure_logging, get_logger

_PACKAGE_ROOT = Path(__file__).resolve().parent.parent

logger = get_logger(__name__)


# =============================================================================
# Provider Environment Variable Mapping
# =============================================================================

PROVIDER_ENV_VARS = {
    "anthropic": ("ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"),
    "openai": ("OPENAI_API_KEY", "OPENAI_BASE_URL"),
    "openai-chat": ("OPENAI_API_KEY", "OPENAI_BASE_URL"),
    "openai-responses": ("OPENAI_API_KEY", "OPENAI_BASE_URL"),
    "google-gla": ("GOOGLE_API_KEY", None),
    "google-vertex": ("GOOGLE_API_KEY", None),
    "gemini": ("GOOGLE_API_KEY", None),
    "groq": ("GROQ_API_KEY", "GROQ_BASE_URL"),
    "bedrock": (None, None),  # Uses AWS credentials
}

# Provider to model_settings preset mapping
PROVIDER_MODEL_SETTINGS = {
    "anthropic": "anthropic_default",
    "openai": "openai_default",
    "openai-chat": "openai_default",
    "openai-responses": "openai_responses_default",
    "google-gla": "gemini_thinking_budget_default",
    "google-vertex": "gemini_thinking_budget_default",
    "gemini": "gemini_thinking_budget_default",
    "groq": None,  # No preset
    "bedrock": None,
}

# Provider to model_cfg preset mapping (context window, capabilities)
# - gemini: vision + video_understanding
# - anthropic/openai: vision only
# - unknown: no capabilities
PROVIDER_MODEL_CFG = {
    "anthropic": "claude_1m",
    "openai": "gpt5_270k",
    "openai-chat": "gpt5_270k",
    "openai-responses": "gpt5_270k",
    "google-gla": "gemini_1m",
    "google-vertex": "gemini_1m",
    "gemini": "gemini_1m",
    "groq": None,  # Unknown - no capabilities
    "bedrock": None,
}


def parse_model_string(model_str: str) -> tuple[str | None, str, str]:
    """Parse model string into (gateway, provider, model_id).

    Format: [gateway@]provider:model_id

    Examples:
        "anthropic:claude-sonnet-4" -> (None, "anthropic", "claude-sonnet-4")
        "mygateway@openai-chat:gpt-4o" -> ("mygateway", "openai", "gpt-4o")
    """
    gateway = None
    if "@" in model_str:
        gateway, model_str = model_str.split("@", 1)

    if ":" not in model_str:
        raise ValueError(f"Invalid model format: {model_str}. Expected 'provider:model_id'")

    provider, model_id = model_str.split(":", 1)
    return gateway, provider, model_id


def get_env_vars_for_model(gateway: str | None, provider: str) -> list[tuple[str, str, bool]]:
    """Get required environment variables for a model.

    Returns:
        List of (env_var_name, description, required) tuples.
    """
    env_vars = []

    if gateway:
        # Gateway mode: {GATEWAY}_API_KEY and {GATEWAY}_BASE_URL
        prefix = gateway.upper()
        env_vars.append((f"{prefix}_API_KEY", "Gateway API Key", True))
        env_vars.append((f"{prefix}_BASE_URL", "Gateway Base URL", True))
    else:
        # Direct provider mode
        api_key, base_url = PROVIDER_ENV_VARS.get(provider, (f"{provider.upper()}_API_KEY", None))
        if api_key:
            env_vars.append((api_key, "API Key", True))
        if base_url:
            env_vars.append((base_url, "Base URL (optional, press Enter to skip)", False))

    return env_vars


# =============================================================================
# Setup Wizard
# =============================================================================


def run_setup_wizard(config_manager: ConfigManager) -> bool:
    """Run interactive setup wizard for first-time configuration.

    Flow:
    1. Copy template config to global config dir
    2. Prompt for model string
    3. Prompt for required environment variables
    4. Update config with values
    5. Show completion message with paths

    Returns:
        True if setup completed successfully, False if user cancelled.
    """
    click.echo()
    click.echo(click.style("Welcome to YAACLI CLI!", fg="cyan", bold=True))
    click.echo("Let's set up your configuration.\n")

    # Step 1: Copy templates to config dir
    config_manager.ensure_config_dir()
    config_path = config_manager.config_dir / "config.toml"
    mcp_path = config_manager.config_dir / "mcp.json"
    subagents_dir = config_manager.config_dir / "subagents"

    # Copy config.toml template
    if not config_path.exists():
        template_path = resources.files("yaacli.templates").joinpath("config.toml")
        with resources.as_file(template_path) as src:
            shutil.copy(src, config_path)
        click.echo(f"Created: {config_path}")

    # Copy mcp.json template (only if not exists - never overwrite user's mcp.json)
    if not mcp_path.exists():
        mcp_template = resources.files("yaacli.templates").joinpath("mcp.json")
        with resources.as_file(mcp_template) as src:
            shutil.copy(src, mcp_path)
        click.echo(f"Created: {mcp_path}")
    else:
        click.echo(f"Skipped: {mcp_path} (already exists)")

    # Copy builtin subagents from ya_agent_sdk (only missing files - never overwrite)
    subagents_dir.mkdir(parents=True, exist_ok=True)
    sdk_presets = resources.files("ya_agent_sdk.subagents.presets")
    copied_subagents = []
    for item in sdk_presets.iterdir():
        if item.name.endswith(".md"):
            target_path = subagents_dir / item.name
            if not target_path.exists():
                with resources.as_file(item) as src:
                    shutil.copy(src, target_path)
                copied_subagents.append(item.name)
    if copied_subagents:
        click.echo(f"Created: {subagents_dir}/ (added: {', '.join(copied_subagents)})")
    else:
        click.echo(f"Skipped: {subagents_dir}/ (all subagents already exist)")

    # Copy builtin skills from yaacli (only missing directories - never overwrite)
    copied_skills = _ensure_builtin_skills(config_manager.config_dir)
    skills_dir = config_manager.config_dir / "skills"
    if copied_skills:
        click.echo(f"Created: {skills_dir}/ (added: {', '.join(copied_skills)})")
    else:
        click.echo(f"Skipped: {skills_dir}/ (all skills already exist)")

    click.echo()

    # Step 2: Prompt for model string
    click.echo(click.style("Step 1: Enter model", bold=True))
    click.echo("  Format: [gateway@]provider:model_id")
    click.echo("  Examples:")
    click.echo("    - anthropic:claude-sonnet-4-20250514")
    click.echo("    - openai-chat:gpt-4o")
    click.echo("    - google-gla:gemini-2.5-pro")
    click.echo("    - mygateway@anthropic:claude-sonnet-4")
    click.echo()

    while True:
        model_str = click.prompt("Model", default="anthropic:claude-sonnet-4-20250514")
        try:
            gateway, provider, _model_id = parse_model_string(model_str)
            break
        except ValueError as e:
            click.echo(click.style(f"  Error: {e}", fg="red"))

    # Step 3: Prompt for environment variables
    click.echo()
    click.echo(click.style("Step 2: Configure credentials", bold=True))

    env_vars = get_env_vars_for_model(gateway, provider)
    env_values: dict[str, str] = {}

    for env_var, description, required in env_vars:
        # Check if already set in environment
        existing = os.environ.get(env_var)
        if existing:
            masked = existing[:8] + "..." if len(existing) > 12 else "***"
            click.echo(f"  {env_var}: found in environment ({masked})")
            if click.confirm("    Use existing value?", default=True):
                continue

        # Prompt for value
        is_secret = "KEY" in env_var or "SECRET" in env_var
        value = click.prompt(
            f"  {env_var} ({description})",
            default="" if not required else None,
            hide_input=is_secret,
            show_default=False,
        )

        if value:
            env_values[env_var] = value
        elif required:
            click.echo(click.style("  This field is required.", fg="red"))
            return False

    # Step 4: Update config file
    click.echo()
    click.echo(click.style("Step 3: Saving configuration...", bold=True))

    # Auto-detect model_settings preset based on provider
    model_settings_preset = PROVIDER_MODEL_SETTINGS.get(provider)
    if model_settings_preset:
        click.echo(f"  Auto-detected model_settings: {model_settings_preset}")

    # Auto-detect model_cfg preset based on provider
    model_cfg_preset = PROVIDER_MODEL_CFG.get(provider)
    if model_cfg_preset:
        click.echo(f"  Auto-detected model_cfg: {model_cfg_preset}")

    # Read current config and update
    config_content = config_path.read_text()

    # Update model
    config_content = re.sub(
        r'^model\s*=\s*".*"',
        f'model = "{model_str}"',
        config_content,
        flags=re.MULTILINE,
    )

    # Update model_settings if we have a preset
    if model_settings_preset:
        # Check for existing uncommented model_settings line
        if re.search(r"^model_settings\s*=", config_content, re.MULTILINE):
            config_content = re.sub(
                r"^model_settings\s*=\s*.*$",
                f'model_settings = "{model_settings_preset}"',
                config_content,
                flags=re.MULTILINE,
            )
        # Check for commented model_settings line and uncomment it
        elif re.search(r"^#\s*model_settings\s*=", config_content, re.MULTILINE):
            config_content = re.sub(
                r"^#\s*model_settings\s*=\s*.*$",
                f'model_settings = "{model_settings_preset}"',
                config_content,
                flags=re.MULTILINE,
            )
        else:
            # Add after model line as last resort
            config_content = re.sub(
                r'^(model\s*=\s*"[^"]*")$',
                f'\\1\nmodel_settings = "{model_settings_preset}"',
                config_content,
                flags=re.MULTILINE,
            )

    # Update model_cfg if we have a preset
    if model_cfg_preset:
        # Check for existing uncommented model_cfg line
        if re.search(r"^model_cfg\s*=", config_content, re.MULTILINE):
            config_content = re.sub(
                r"^model_cfg\s*=\s*.*$",
                f'model_cfg = "{model_cfg_preset}"',
                config_content,
                flags=re.MULTILINE,
            )
        # Check for commented model_cfg line and uncomment it
        elif re.search(r"^#\s*model_cfg\s*=", config_content, re.MULTILINE):
            config_content = re.sub(
                r"^#\s*model_cfg\s*=\s*.*$",
                f'model_cfg = "{model_cfg_preset}"',
                config_content,
                flags=re.MULTILINE,
            )
        else:
            # Add after model_settings line (or model line if no model_settings)
            if re.search(r"^model_settings\s*=", config_content, re.MULTILINE):
                config_content = re.sub(
                    r'^(model_settings\s*=\s*"[^"]*")$',
                    f'\\1\nmodel_cfg = "{model_cfg_preset}"',
                    config_content,
                    flags=re.MULTILINE,
                )
            else:
                config_content = re.sub(
                    r'^(model\s*=\s*"[^"]*")$',
                    f'\\1\nmodel_cfg = "{model_cfg_preset}"',
                    config_content,
                    flags=re.MULTILINE,
                )

    # Update [env] section with new values
    if env_values:
        # Find or create [env] section
        if "[env]" in config_content:
            # Add values after [env]
            env_lines = "\n".join(f'{k} = "{v}"' for k, v in env_values.items())
            config_content = re.sub(
                r"\[env\]\n(#[^\n]*\n)*",
                f"[env]\n{env_lines}\n",
                config_content,
            )
        else:
            # Append [env] section
            env_lines = "\n".join(f'{k} = "{v}"' for k, v in env_values.items())
            config_content += f"\n[env]\n{env_lines}\n"

    config_path.write_text(config_content)

    # Step 5: Show completion
    click.echo()
    click.echo(click.style("Setup complete!", fg="green", bold=True))
    click.echo()
    click.echo("Configuration saved to:")
    click.echo(f"  {config_path}")
    click.echo()
    click.echo("You can also configure:")
    click.echo(f"  - Custom subagents: {config_manager.config_dir / 'subagents/'}")
    click.echo(f"  - MCP servers: {config_manager.config_dir / 'mcp.json'}")
    click.echo()
    click.echo("Run 'yaacli' again to start!")
    click.echo()

    return True


def load_package_env_files() -> None:
    """Load .env files for repo development and local invocation.

    Priority favors the current working directory over the package directory.
    """
    load_dotenv(_PACKAGE_ROOT / ".env", override=False)
    load_dotenv(override=False)


def load_env_from_config(config: YaacliConfig) -> None:
    """Load environment variables from config [env] section."""
    if config.env:
        for key, value in config.env.items():
            if value and key not in os.environ:
                os.environ[key] = value


def _ensure_builtin_skills(config_dir: Path) -> list[str]:
    """Ensure builtin skills are copied to config directory.

    Copies builtin skills from yaacli.skills to the config directory.
    Only copies missing skill directories - never overwrites existing ones.

    Args:
        config_dir: Global config directory path.

    Returns:
        List of skill names that were copied.
    """
    skills_dir = config_dir / "skills"
    skills_dir.mkdir(parents=True, exist_ok=True)

    builtin_skills = resources.files("yaacli.skills")
    copied_skills: list[str] = []

    for item in builtin_skills.iterdir():
        # Skip __init__.py and other non-skill files
        if item.name.startswith(("_", ".")):
            continue

        # Check if this is a skill directory (has SKILL.md)
        try:
            skill_md = item.joinpath("SKILL.md")
            if not skill_md.is_file():
                continue
        except (TypeError, AttributeError):
            # Not a directory or can't check
            continue

        target_dir = skills_dir / item.name
        if target_dir.exists():
            # Skip existing skills - never overwrite user modifications
            continue

        # Copy the entire skill directory
        with resources.as_file(item) as src_dir:
            shutil.copytree(src_dir, target_dir)
        copied_skills.append(item.name)

    return copied_skills


def ensure_builtin_assets(config_manager: ConfigManager) -> None:
    """Ensure builtin assets (subagents, skills) exist in config directory.

    This is called on every CLI startup to ensure new builtin assets
    from package updates are available to users.

    Args:
        config_manager: Configuration manager instance.
    """
    config_dir = config_manager.config_dir

    # Ensure config directory exists
    config_dir.mkdir(parents=True, exist_ok=True)

    # Ensure subagents directory and copy missing presets
    subagents_dir = config_dir / "subagents"
    subagents_dir.mkdir(exist_ok=True)
    try:
        sdk_presets = resources.files("ya_agent_sdk.subagents.presets")
        for item in sdk_presets.iterdir():
            if item.name.endswith(".md"):
                target_path = subagents_dir / item.name
                if not target_path.exists():
                    with resources.as_file(item) as src:
                        shutil.copy(src, target_path)
                    logger.debug(f"Copied builtin subagent: {item.name}")
    except Exception as e:
        logger.warning(f"Failed to copy builtin subagents: {e}")

    # Ensure skills directory and copy missing skills
    copied_skills = _ensure_builtin_skills(config_dir)
    if copied_skills:
        logger.info(f"Copied builtin skills: {', '.join(copied_skills)}")


# =============================================================================
# Git Worktree Support
# =============================================================================


def _get_git_root() -> Path | None:
    """Get the root directory of the current git repository.

    Returns:
        Path to git root, or None if not in a git repo.
    """
    try:
        result = subprocess.run(
            ["git", "rev-parse", "--show-toplevel"],  # noqa: S607
            capture_output=True,
            text=True,
            check=True,
        )
        return Path(result.stdout.strip())
    except (subprocess.CalledProcessError, FileNotFoundError):
        return None


def _project_hash(git_root: Path) -> str:
    """Generate stable hash from git root path.

    Returns:
        A 12-character hex string derived from SHA256 of the absolute path.
    """
    path_str = str(git_root.resolve())
    return hashlib.sha256(path_str.encode()).hexdigest()[:12]


def _create_worktree(branch_name: str | None) -> tuple[Path, str, bool]:
    """Create or resume a git worktree for isolated agent work.

    If a worktree with the given branch name already exists, it will be
    reused (resumed) instead of creating a new one.

    Args:
        branch_name: Branch name to create. If None, auto-generates one.

    Returns:
        Tuple of (worktree_path, branch_name, is_resume).

    Raises:
        click.ClickException: If not in a git repo or worktree creation fails.
    """
    git_root = _get_git_root()
    if git_root is None:
        raise click.ClickException("--worktree requires a git repository, but none was found.")

    # Auto-generate branch name if not provided
    if branch_name is None:
        timestamp = datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
        branch_name = f"yaacli/{timestamp}"

    # Create worktree directory under ~/.yaacli/worktrees/{project_hash}/{branch}
    proj_hash = _project_hash(git_root)
    worktrees_dir = ConfigManager.DEFAULT_CONFIG_DIR / "worktrees" / proj_hash
    worktrees_dir.mkdir(parents=True, exist_ok=True)
    worktree_dir = worktrees_dir / branch_name.replace("/", "-")

    # Write metadata for discoverability
    metadata_file = worktrees_dir / "metadata.json"
    if not metadata_file.exists():
        metadata: WorktreeMetadata = {
            "git_root": str(git_root.resolve()),
            "created_at": datetime.now(UTC).isoformat(),
        }
        metadata_file.write_text(json.dumps(metadata, indent=2))

    # Resume if worktree already exists
    if worktree_dir.exists():
        return worktree_dir, branch_name, True

    try:
        subprocess.run(  # noqa: S603
            ["git", "worktree", "add", str(worktree_dir), "-b", branch_name],  # noqa: S607
            capture_output=True,
            text=True,
            check=True,
            cwd=str(git_root),
        )
    except subprocess.CalledProcessError as e:
        stderr = e.stderr.strip() if e.stderr else "Unknown error"
        raise click.ClickException(f"Failed to create git worktree: {stderr}") from e

    return worktree_dir, branch_name, False


# =============================================================================
# CLI Entry Point
# =============================================================================


def _prepare_cli_runtime(verbose: bool) -> tuple[ConfigManager, YaacliConfig]:
    configure_logging(verbose=verbose)
    logger.info("Starting yaacli v%s", __version__)
    load_package_env_files()
    config_manager = ConfigManager()
    config = config_manager.load()
    ensure_builtin_assets(config_manager)
    if not config.is_configured:
        if not run_setup_wizard(config_manager):
            raise click.exceptions.Exit(0)
        config = config_manager.reload()
    load_env_from_config(config)
    return config_manager, config


def _prepare_session_cli_runtime(verbose: bool) -> ConfigManager:
    configure_logging(verbose=verbose)
    logger.info("Starting yaacli sessions command v%s", __version__)
    config_manager = ConfigManager()
    config_manager.ensure_config_dir()
    return config_manager


@click.group(invoke_without_command=True)
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose logging")
@click.option("-w", "--worktree", is_flag=True, default=False, help="Run in a git worktree.")
@click.option(
    "-b",
    "--branch",
    "worktree_branch",
    default=None,
    metavar="BRANCH",
    help="Branch name for worktree (implies --worktree).",
)
@click.option("-p", "--prompt", default=None, metavar="PROMPT", help="Run one prompt in headless mode.")
@click.option(
    "--worker",
    is_flag=True,
    default=False,
    help="Run headless as a worker without synchronous delegate subagents.",
)
@click.option("-s", "--session", "session_id", default=None, metavar="SESSION_ID", help="Restore a saved session.")
@click.option(
    "--profile",
    "model_profile_id",
    default=None,
    metavar="PROFILE_ID",
    help="Model profile ID to use for this run.",
)
@click.option(
    "--model-profile",
    "model_profile_id_alias",
    default=None,
    metavar="PROFILE_ID",
    help="Alias for --profile.",
)
@click.version_option(version=__version__, prog_name="yaacli")
@click.pass_context
def cli(
    ctx: click.Context,
    verbose: bool,
    worktree: bool,
    worktree_branch: str | None,
    prompt: str | None,
    worker: bool,
    session_id: str | None,
    model_profile_id: str | None,
    model_profile_id_alias: str | None,
) -> None:
    """YAACLI CLI - AI-powered coding assistant.

    Inside TUI, use slash commands:
      /help     - Show available commands
      /config   - Show/edit configuration
      /mode     - Switch between act/plan modes
      /loop     - Run task in autonomous loop
      /tasks    - Show background tasks and processes
      /session  - List/restore sessions
      /dump     - Save session to folder
      /load     - Load session from folder
      /clear    - Clear conversation
      /exit     - Exit application

    Headless mode:
      yaacli -p "Fix the failing tests"
      yaacli -p "Continue" --session <session-id> --profile <profile-id>

    Session commands:
      yaacli sessions list
      yaacli sessions show <session-id>
    """
    if ctx.invoked_subcommand is not None:
        return

    effective_model_profile_id = model_profile_id_alias or model_profile_id
    if worker and prompt is None:
        raise click.UsageError("--worker requires --prompt/-p headless mode.")

    config_manager, config = _prepare_cli_runtime(verbose)

    # Set up worktree if requested
    worktree_dir: Path | None = None
    actual_branch: str | None = None
    if worktree or worktree_branch is not None:
        worktree_dir, actual_branch, is_resume = _create_worktree(worktree_branch)
        if is_resume:
            click.echo(click.style("Resuming worktree:", fg="cyan", bold=True))
        else:
            click.echo(click.style("Worktree created:", fg="cyan", bold=True))
        click.echo(f"  Branch:    {actual_branch}")
        click.echo(f"  Directory: {worktree_dir}")
        click.echo()

    working_dir = worktree_dir or Path.cwd()

    # Run the selected frontend
    exit_code = 0
    completed_session_id: str | None = None
    try:
        if prompt is not None:
            completed_session_id = asyncio.run(
                _run_headless_prompt(
                    config,
                    config_manager,
                    prompt,
                    working_dir=working_dir,
                    session_id=session_id,
                    model_profile_id=effective_model_profile_id,
                    worker=worker,
                )
            )
        else:
            completed_session_id = asyncio.run(
                _run_tui(
                    config,
                    config_manager,
                    verbose,
                    working_dir=working_dir,
                    model_profile_id=effective_model_profile_id,
                )
            )
    except KeyboardInterrupt:
        click.echo("\nGoodbye!")
        exit_code = 130
    except Exception as e:
        logger.exception("Fatal error")
        click.echo()
        click.echo(click.style("=" * 60, fg="red"))
        click.echo(click.style("FATAL ERROR", fg="red", bold=True))
        click.echo(click.style("=" * 60, fg="red"))
        click.echo()
        click.echo(f"Error type: {type(e).__name__}")
        click.echo(f"Message: {e}")
        click.echo()
        # Show traceback in verbose mode or for unexpected errors
        if verbose:
            import traceback

            click.echo(click.style("Traceback:", fg="yellow"))
            click.echo(traceback.format_exc())
        else:
            click.echo("Run with --verbose flag for full traceback.")
        click.echo()
        click.echo("Common issues:")
        click.echo("  - API key not set or invalid")
        click.echo("  - Network connectivity issues")
        click.echo("  - Invalid model configuration")
        click.echo()
        click.echo(f"Check logs at: {LOG_FILE_NAME} (with --verbose flag)")
        exit_code = 1

    # Show resume hints on exit
    if completed_session_id or worktree_dir is not None:
        click.echo()

    if completed_session_id:
        click.echo(click.style(f"Session: {completed_session_id}", fg="cyan", bold=True), err=prompt is not None)
        click.echo(err=prompt is not None)
        click.echo("To resume this session:", err=prompt is not None)
        if prompt is not None:
            click.echo(f"  yaacli -p '<prompt>' -s {completed_session_id}", err=True)
        else:
            click.echo(f"  /session {completed_session_id}")

    if worktree_dir is not None:
        click.echo()
        click.echo(click.style("Worktree is still available:", fg="cyan", bold=True))
        click.echo(f"  Directory: {worktree_dir}")
        click.echo()
        click.echo("To resume in this worktree:")
        click.echo(f"  yaacli -w -b {actual_branch}")
        click.echo()
        click.echo("To remove when done:")
        click.echo(f"  git worktree remove {worktree_dir}")

    sys.exit(exit_code)


async def _run_headless_prompt(
    config: YaacliConfig,
    config_manager: ConfigManager,
    prompt: str,
    *,
    working_dir: Path | None = None,
    session_id: str | None = None,
    model_profile_id: str | None = None,
    worker: bool = False,
) -> str | None:
    """Run one prompt in headless mode and stream display events as NDJSON."""
    from yaacli.headless import run_headless_prompt

    result = await run_headless_prompt(
        config=config,
        config_manager=config_manager,
        prompt=prompt,
        working_dir=working_dir or Path.cwd(),
        session_id=session_id,
        model_profile_id=model_profile_id,
        worker=worker,
    )
    return result.session_id


async def _run_tui(
    config: YaacliConfig,
    config_manager: ConfigManager,
    verbose: bool,
    *,
    working_dir: Path | None = None,
    model_profile_id: str | None = None,
) -> str | None:
    """Run the TUI application.

    Returns:
        Session ID if the session has saved data, None otherwise.
    """
    from yaacli.app import TUIApp
    from yaacli.model_profiles import get_model_profile

    model_profile = None
    if model_profile_id:
        model_profile = get_model_profile(config, model_profile_id)
        if model_profile is None:
            raise click.ClickException(f"Unknown model profile: {model_profile_id}")

    async with TUIApp(
        config=config,
        config_manager=config_manager,
        verbose=verbose,
        working_dir=working_dir or Path.cwd(),
    ) as app:
        if model_profile is not None:
            await app._switch_model_profile(model_profile)
        await app.run()
        return app.session_id if app.has_session_data else None


@cli.group()
def sessions() -> None:
    """Manage saved YAACLI sessions."""


def _session_info_payload(entry: object) -> dict[str, object]:
    payload = {
        name: getattr(entry, name)
        for name in (
            "id",
            "path",
            "updated_at",
            "created_at",
            "working_dir",
            "output_text",
            "message_count",
            "display_event_count",
            "metadata",
        )
    }
    payload["path"] = str(payload["path"])
    return payload


@sessions.command("list")
@click.option("--json", "as_json", is_flag=True, help="Emit JSON instead of a table.")
@click.option("--limit", default=20, show_default=True, type=int, help="Maximum sessions to display.")
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose logging")
def sessions_list(as_json: bool, limit: int, verbose: bool) -> None:
    """List saved sessions."""
    from yaacli.sessions import list_sessions

    config_manager = _prepare_session_cli_runtime(verbose)
    entries = list_sessions(config_manager)[: max(limit, 0)]
    if as_json:
        click.echo(json.dumps([_session_info_payload(entry) for entry in entries], ensure_ascii=False, indent=2))
        return

    if not entries:
        click.echo("No sessions found.")
        return

    click.echo("Session ID     Updated              Messages  Events  Working Dir")
    for entry in entries:
        updated = entry.updated_at[:19].replace("T", " ")
        message_count = "-" if entry.message_count is None else str(entry.message_count)
        event_count = "-" if entry.display_event_count is None else str(entry.display_event_count)
        click.echo(f"{entry.id:<14} {updated:<20} {message_count:<8} {event_count:<6} {entry.working_dir or '-'}")


@sessions.command("show")
@click.argument("session_id")
@click.option("--json", "as_json", is_flag=True, help="Emit JSON details.")
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose logging")
def sessions_show(session_id: str, as_json: bool, verbose: bool) -> None:
    """Show a saved session."""
    from yaacli.sessions import get_session_info

    config_manager = _prepare_session_cli_runtime(verbose)
    entry = get_session_info(config_manager, session_id)
    payload = _session_info_payload(entry)
    if as_json:
        click.echo(json.dumps(payload, ensure_ascii=False, indent=2))
        return

    click.echo(f"Session: {entry.id}")
    click.echo(f"Path: {entry.path}")
    click.echo(f"Created: {entry.created_at or '-'}")
    click.echo(f"Updated: {entry.updated_at}")
    click.echo(f"Working dir: {entry.working_dir or '-'}")
    click.echo(f"Messages: {entry.message_count if entry.message_count is not None else '-'}")
    click.echo(f"Display events: {entry.display_event_count if entry.display_event_count is not None else '-'}")
    if entry.output_text:
        click.echo("Output:")
        click.echo(entry.output_text)


@sessions.command("delete")
@click.argument("session_id")
@click.option("--yes", is_flag=True, help="Skip confirmation.")
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose logging")
def sessions_delete(session_id: str, yes: bool, verbose: bool) -> None:
    """Delete a saved session."""
    from yaacli.sessions import delete_session, get_session_info

    config_manager = _prepare_session_cli_runtime(verbose)
    entry = get_session_info(config_manager, session_id)
    if not yes and not click.confirm(f"Delete session {entry.id}?"):
        click.echo("Cancelled.")
        return
    deleted = delete_session(config_manager, entry.id)
    click.echo(f"Deleted session: {deleted.id}")


def main() -> None:
    """Main entry point."""
    cli()


if __name__ == "__main__":
    main()
