"""CLI entry point for OpenClaw Sandbox CLI."""

import functools
import os
import secrets
import subprocess
import sys

import click
from dotenv import load_dotenv
from rich.panel import Panel
from rich.table import Table

from .const import (
    API_KEY_URL,
    BRAND_NAME,
    CLI_NAME,
    CONFIG_SECTION,
    ENV_VAR_API_KEY,
    PROVIDER_NAME,
    TEMPLATE_ID,
)
from .config import load_config
from .openclaw import OPENCLAW_CONFIG_PATH, configure_openclaw, get_gateway_token, get_public_urls
from .output import (
    EXIT_OPENCLAW_NOT_INSTALLED,
    CLIError,
    DoctorError,
    GatewayTimeoutError,
    MissingApiKeyError,
    is_json_mode,
    output_error,
    output_success,
    set_json_mode,
)
from .sandbox import SANDBOX_KEEP_ALIVE, SandboxManager
from .utils import console, get_console, print_error, print_info, print_success

load_dotenv()

_API_KEY_HELP = "{provider} API key. Get yours at {url}".format(
    provider=PROVIDER_NAME, url=API_KEY_URL,
)

# --- Helpers ---


def _print_api_key_guide():
    """Print API key setup guide and exit."""
    if is_json_mode():
        output_error(
            "MISSING_API_KEY",
            "{provider} API key is required. Set via --api-key, {env} env, or config file.".format(
                provider=PROVIDER_NAME, env=ENV_VAR_API_KEY,
            ),
            10,
        )
        return  # output_error raises SystemExit, but guard explicitly

    print_error("{provider} API key is required. Configure it using one of these methods:".format(
        provider=PROVIDER_NAME,
    ))
    console.print()
    console.print("  1. Pass directly (launch only):")
    console.print("     [cyan]{cli} launch --api-key sk_your_api_key[/cyan]".format(cli=CLI_NAME))
    console.print()
    console.print("  2. Set for current command:")
    console.print("     [cyan]{env}=sk_your_api_key {cli} <command>[/cyan]".format(
        env=ENV_VAR_API_KEY, cli=CLI_NAME,
    ))
    console.print()
    console.print("  3. Export for current session:")
    console.print("     [cyan]export {env}=sk_your_api_key[/cyan]".format(env=ENV_VAR_API_KEY))
    console.print()
    console.print("  4. Add to shell profile (~/.bashrc or ~/.zshrc):")
    console.print("     [cyan]echo 'export {env}=sk_your_api_key' >> ~/.zshrc[/cyan]".format(
        env=ENV_VAR_API_KEY,
    ))
    console.print()
    console.print("  Get your API key at: [link={url}]{url}[/link]".format(url=API_KEY_URL))
    raise MissingApiKeyError()


def _resolve_api_key(kwargs) -> str:
    """Resolve API key from kwargs/env/config or raise."""
    config = load_config()
    key = kwargs.get("api_key") or config[CONFIG_SECTION].get("api_key")
    if not key:
        _print_api_key_guide()
    return key


def require_api_key(f):
    """Decorator that resolves the API key and injects it into kwargs."""
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        kwargs["api_key"] = _resolve_api_key(kwargs)
        return f(*args, **kwargs)
    return wrapper


def _print_sandbox_info(sandbox_id, gateway_token, urls, keep_alive):
    """Print sandbox launch summary."""
    summary = (
        "[cyan]Sandbox ID:[/cyan]         {sandbox_id}\n"
        "[cyan]Keep Alive:[/cyan]         {keep_alive}s\n"
        "\n"
        "[cyan]Web UI:[/cyan]             {webui}\n"
        "[cyan]Gateway WebSocket:[/cyan]  {gateway_ws}\n"
        "[cyan]Gateway Token:[/cyan]      {gateway_token}"
    ).format(
        sandbox_id=sandbox_id,
        keep_alive=keep_alive,
        webui=urls["webui"],
        gateway_ws=urls["gateway_ws"],
        gateway_token=gateway_token,
    )
    console.print(Panel(
        summary,
        title="[green]{brand} Sandbox Ready[/green]".format(brand=BRAND_NAME),
        border_style="green",
    ))

    print_info("")
    print_info("Open Web UI in browser:")
    print_info("  {webui}".format(webui=urls["webui"]))
    print_info("")
    print_info("Connect via TUI:")
    print_info("  {cli} tui {sandbox_id} --token {token}".format(
        cli=CLI_NAME, sandbox_id=sandbox_id, token=gateway_token,
    ))
    print_info("  (Device pairing will be auto-approved within 3s, please wait)")
    print_info("")
    print_info("Or use openclaw CLI directly:")
    print_info("  openclaw tui --url {ws} --token {token}".format(
        ws=urls["gateway_ws"], token=gateway_token,
    ))
    print_info("  (Device pairing will be auto-approved within 3s, please wait)")


def _fmt_state(sbx) -> str:
    """Format sandbox state as a plain string."""
    return str(sbx.state.value) if hasattr(sbx.state, "value") else str(sbx.state)


def _handle_cli_error(e: CLIError):
    """Handle a CLIError -- JSON output or Rich error + exit."""
    if is_json_mode():
        output_error(e.error_code, str(e), e.exit_code)
    else:
        print_error(str(e))
        raise SystemExit(e.exit_code)


# --- CLI ---


@click.group()
@click.version_option(package_name=CLI_NAME)
@click.option("--json", "-j", "json_output", is_flag=True, help="Output as JSON (for programmatic use). AI agents should always enable this flag.")
def cli(json_output):
    set_json_mode(json_output)


def _json_option(f):
    """Add --json/-j to a subcommand, merging with the group-level flag."""
    @click.option("--json", "-j", "json_output", is_flag=True, hidden=True)
    @functools.wraps(f)
    def wrapper(*args, json_output=False, **kwargs):
        if json_output:
            set_json_mode(True)
        return f(*args, **kwargs)
    return wrapper


cli.help = "{brand} - One-click launch of OpenClaw on {provider} Agent Sandbox.".format(
    brand=BRAND_NAME, provider=PROVIDER_NAME,
)



@cli.command()
@click.option("--api-key", default=None, help=_API_KEY_HELP + " (also used for OpenClaw LLM)")
@click.option("--gateway-token", default=None, help="Gateway auth token for OpenClaw access control. Recommend setting a secure custom token; auto-generated if not set.")
@click.option("--timeout", default=60, help="Sandbox creation timeout in seconds")
@_json_option
@require_api_key
def launch(api_key, gateway_token, timeout):
    """Launch a new OpenClaw sandbox environment."""
    try:
        gw_token = gateway_token or os.environ.get("OPENCLAW_GATEWAY_TOKEN") or secrets.token_urlsafe(16)

        c = get_console()

        # Step 1: Create sandbox
        with c.status("[bold blue]Creating sandbox..."):
            manager = SandboxManager(TEMPLATE_ID, api_key=api_key)
            sandbox = manager.create(timeout=timeout)

        # Step 2: Extend sandbox lifetime (7 days)
        with c.status("[bold blue]Setting sandbox keep-alive..."):
            sandbox.set_timeout(SANDBOX_KEEP_ALIVE)

        # Step 3: Configure OpenClaw
        with c.status("[bold blue]Configuring OpenClaw..."):
            configure_openclaw(manager, api_key, gw_token)

        # Step 4: Get public URLs
        with c.status("[bold blue]Fetching public URLs..."):
            urls = get_public_urls(manager)

        # Append token to WebUI URL so Control UI auto-authenticates
        webui_url = "{base}?token={token}".format(base=urls["webui"], token=gw_token)

        if is_json_mode():
            output_success({
                "sandbox_id": sandbox.sandbox_id,
                "keep_alive": SANDBOX_KEEP_ALIVE,
                "webui": webui_url,
                "gateway_ws": urls["gateway_ws"],
                "gateway_token": gw_token,
            })
        else:
            urls["webui"] = webui_url
            _print_sandbox_info(sandbox.sandbox_id, gw_token, urls, SANDBOX_KEEP_ALIVE)

    except CLIError as e:
        _handle_cli_error(e)


@cli.command()
@click.argument("sandbox_id")
@click.option("--api-key", default=None, help=_API_KEY_HELP)
@_json_option
@require_api_key
def stop(sandbox_id, api_key):
    """Stop and terminate a sandbox."""
    try:
        manager = SandboxManager(api_key=api_key)
        manager.connect(sandbox_id)

        c = get_console()
        with c.status("[bold red]Stopping sandbox..."):
            manager.kill()

        if is_json_mode():
            output_success({
                "sandbox_id": sandbox_id,
                "message": "Sandbox terminated",
            })

    except CLIError as e:
        _handle_cli_error(e)


@cli.command("list")
@click.option("--api-key", default=None, help=_API_KEY_HELP)
@_json_option
@require_api_key
def list_sandboxes(api_key):
    """List all active sandboxes."""
    try:
        c = get_console()
        with c.status("[bold blue]Fetching sandboxes..."):
            sandboxes = SandboxManager.list_app_sandboxes(api_key=api_key)

        if is_json_mode():
            output_success({
                "sandboxes": [
                    {
                        "sandbox_id": sbx.sandbox_id,
                        "state": _fmt_state(sbx),
                        "cpu": sbx.cpu_count,
                        "memory_mb": sbx.memory_mb,
                        "started_at": sbx.started_at.isoformat(),
                    }
                    for sbx in sandboxes
                ],
            })
            return

        if not sandboxes:
            print_info("No {brand} sandboxes found.".format(brand=BRAND_NAME))
            return

        table = Table(title="{brand} Sandboxes".format(brand=BRAND_NAME))
        table.add_column("Sandbox ID", style="cyan")
        table.add_column("State", style="bold")
        table.add_column("CPU", justify="right")
        table.add_column("Memory (MB)", justify="right")
        table.add_column("Started At", style="yellow")

        for sbx in sandboxes:
            table.add_row(
                sbx.sandbox_id,
                _fmt_state(sbx),
                str(sbx.cpu_count),
                str(sbx.memory_mb),
                sbx.started_at.strftime("%Y-%m-%d %H:%M:%S"),
            )

        console.print(table)

    except CLIError as e:
        _handle_cli_error(e)


@cli.command()
@click.argument("sandbox_id")
@click.option("--api-key", default=None, help=_API_KEY_HELP)
@_json_option
@require_api_key
def status(sandbox_id, api_key):
    """Check sandbox status and show access URLs."""
    try:
        manager = SandboxManager(api_key=api_key)
        manager.connect(sandbox_id)

        sandbox_status = manager.get_status()
        urls = get_public_urls(manager)
        gw_token = get_gateway_token(manager)

        # Append token to WebUI URL for auto-authentication
        webui_url = urls["webui"]
        if gw_token:
            webui_url = "{base}?token={token}".format(base=webui_url, token=gw_token)

        if is_json_mode():
            result = {
                "sandbox_id": sandbox_id,
                "state": sandbox_status,
                "webui": webui_url,
                "gateway_ws": urls["gateway_ws"],
            }
            if gw_token:
                result["gateway_token"] = gw_token
            output_success(result)
            return

        summary = (
            "[cyan]Sandbox ID:[/cyan]     {sandbox_id}\n"
            "[cyan]Status:[/cyan]         {status}\n"
            "[cyan]Web UI:[/cyan]         {webui}\n"
            "[cyan]Gateway WS:[/cyan]     {gateway_ws}\n"
            "[cyan]Gateway Token:[/cyan]  {gateway_token}"
        ).format(
            sandbox_id=sandbox_id,
            status=sandbox_status,
            webui=webui_url,
            gateway_ws=urls["gateway_ws"],
            gateway_token=gw_token or "(not found)",
        )
        console.print(Panel(summary, title="Status", border_style="blue"))
        if gw_token:
            print_info("Connect via TUI:")
            print_info("  {cli} tui {sandbox_id} --token {token}".format(
                cli=CLI_NAME, sandbox_id=sandbox_id, token=gw_token,
            ))

    except CLIError as e:
        _handle_cli_error(e)


@cli.command()
@click.argument("sandbox_id")
@click.option("--api-key", default=None, help=_API_KEY_HELP)
@click.option("--deep", is_flag=True, help="Scan system services for extra gateway installs")
@click.option("--fix", is_flag=True, help="Apply recommended repairs (alias for --repair)")
@click.option("--force", is_flag=True, help="Apply aggressive repairs (overwrites custom service config)")
@click.option("--generate-gateway-token", is_flag=True, help="Generate and configure a gateway token")
@click.option("--no-workspace-suggestions", is_flag=True, help="Disable workspace memory system suggestions")
@click.option("--repair", is_flag=True, help="Apply recommended repairs without prompting")
@click.option("--yes", "yes_flag", is_flag=True, help="Accept defaults without prompting")
@_json_option
@require_api_key
def doctor(sandbox_id, api_key, deep, fix, force, generate_gateway_token,
           no_workspace_suggestions, repair, yes_flag):
    """Run OpenClaw doctor inside a sandbox to diagnose and repair issues.

    Checks configuration integrity, file permissions, gateway health,
    plugin/skill status, and memory search setup. Use --fix or --repair
    to auto-apply recommended fixes; add --force for aggressive repairs.

    Always runs with --non-interactive (sandbox has no TTY).
    See https://docs.openclaw.ai/gateway/doctor for details.
    """
    try:
        manager = SandboxManager(api_key=api_key)
        manager.connect(sandbox_id)

        # Build command — always non-interactive (no TTY in sandbox)
        cmd_parts = [
            "OPENCLAW_CONFIG_PATH={config}".format(config=OPENCLAW_CONFIG_PATH),
            "openclaw", "doctor", "--non-interactive",
        ]
        flags = {
            "--deep": deep,
            "--fix": fix,
            "--force": force,
            "--generate-gateway-token": generate_gateway_token,
            "--no-workspace-suggestions": no_workspace_suggestions,
            "--repair": repair,
            "--yes": yes_flag,
        }
        cmd_parts.extend(flag for flag, enabled in flags.items() if enabled)

        cmd = " ".join(cmd_parts) + " 2>&1"

        c = get_console()
        with c.status("[bold blue]Running OpenClaw doctor..."):
            result = manager.run_command(cmd, timeout=120)

        if result is None:
            raise DoctorError(
                "Doctor command failed to execute in sandbox. "
                "Check sandbox connectivity and that openclaw is installed."
            )

        output_text = ""
        if result.stdout:
            output_text = result.stdout.strip()

        if is_json_mode():
            output_success({
                "sandbox_id": sandbox_id,
                "output": output_text,
            })
            return

        if output_text:
            console.print(output_text, highlight=False)
        else:
            print_info("No output from doctor command.")

    except CLIError as e:
        _handle_cli_error(e)


@cli.command()
@click.argument("sandbox_id")
@click.option("--token", required=True, help="Gateway auth token")
@click.option("--api-key", default=None, help=_API_KEY_HELP)
@_json_option
@require_api_key
def tui(sandbox_id, token, api_key):
    """Connect to an OpenClaw sandbox via TUI."""
    try:
        manager = SandboxManager(api_key=api_key)
        manager.connect(sandbox_id)

        urls = get_public_urls(manager)
        ws_url = urls["gateway_ws"]

        if is_json_mode():
            output_success({
                "sandbox_id": sandbox_id,
                "gateway_ws": ws_url,
                "command": "openclaw tui --url {ws} --token {token}".format(ws=ws_url, token=token),
            })
            return

        cmd = ["openclaw", "tui", "--url", ws_url, "--token", token]
        print_info("Connecting to OpenClaw TUI...")
        print_info("  {cmd}".format(cmd=" ".join(cmd)))

        try:
            subprocess.run(cmd, check=True)
        except FileNotFoundError:
            print_error("'openclaw' CLI not found. Install it with: npm install -g openclaw")
            raise SystemExit(EXIT_OPENCLAW_NOT_INSTALLED)
        except subprocess.CalledProcessError as e:
            print_error("TUI exited with code {code}".format(code=e.returncode))
            raise SystemExit(e.returncode)

    except CLIError as e:
        _handle_cli_error(e)


if __name__ == "__main__":
    cli()
