"""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 yaacli import __version__  # pyright: ignore[reportAttributeAccessIssue]
from yaacli.config import ConfigManager, WorktreeMetadata, YaacliConfig
from yaacli.logging import LOG_FILE_NAME, configure_logging, get_logger

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_200k",
    "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: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: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_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
# =============================================================================


@click.command()
@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.version_option(version=__version__, prog_name="yaacli")
def cli(verbose: bool, worktree: bool, worktree_branch: 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
      /session  - List/restore sessions
      /dump     - Save session to folder
      /load     - Load session from folder
      /clear    - Clear conversation
      /exit     - Exit application
    """
    configure_logging(verbose=verbose)
    logger.info("Starting yaacli v%s", __version__)

    # Load configuration
    config_manager = ConfigManager()
    config = config_manager.load()

    # Ensure builtin assets exist (subagents, skills)
    # This runs on every startup to pick up new assets from package updates
    ensure_builtin_assets(config_manager)

    # Check if configuration exists
    if not config.is_configured:
        if not run_setup_wizard(config_manager):
            sys.exit(0)
        # Reload config after setup
        config = config_manager.reload()

    # Load env vars from config
    load_env_from_config(config)

    # 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 TUI
    exit_code = 0
    session_id: str | None = None
    try:
        session_id = asyncio.run(_run_tui(config, config_manager, verbose, working_dir=working_dir))
    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 session_id or worktree_dir is not None:
        click.echo()

    if session_id:
        click.echo(click.style(f"Session: {session_id}", fg="cyan", bold=True))
        click.echo()
        click.echo("To resume this session:")
        click.echo(f"  /session {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_tui(
    config: YaacliConfig,
    config_manager: ConfigManager,
    verbose: bool,
    *,
    working_dir: Path | None = None,
) -> str | None:
    """Run the TUI application.

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

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


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


if __name__ == "__main__":
    main()
