"""Install, list, stop, uninstall, cleanup commands. Depends on common, registry_helpers, core_lifecycle."""

import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any, Callable, Optional


from rich.panel import Panel
from rich.table import Table

from armorer.profiles import RiskProfile, FilesystemPolicy, NetworkPolicy
from armorer.registry import AppRegistry
from armorer.cli import common as cli_common
from armorer.cli import registry_helpers as cli_registry
from armorer.cli import core_lifecycle as cli_lifecycle
from armorer import docker_runner


def is_ignorable_cli_exit(container_name: str, status_text: str) -> bool:
    name = (container_name or "").lower()
    status = (status_text or "").lower()
    return "cli" in name and "exited" in status


def install_impl(
    ctx: Any,
    agent: str,
    yes: bool,
    skip_scan: bool,
    alias: Optional[str],
    use: Optional[str],
    *,
    run_callback: Callable,
    print_logo: Callable[[], None],
    check_notifications: Callable[[], None],
    first_setup_run_command: Callable,
    run_armorer_managed_openclaw_setup: Callable[[str], int],
    is_profile_only_compose: Callable[[Path], bool],
    ui_confirm: Callable[[str, bool], bool],
    infer_agent_id: Callable[[str], str],
    instances_for_agent: Callable[[AppRegistry, str], list],
    next_instance_alias: Callable[[AppRegistry, str], str],
    get_app_with_openclaw_fallback: Callable[[AppRegistry, str], Any],
    can_reinstall_stale_default_openclaw_alias: Callable[[AppRegistry, str, str], bool],
) -> None:
    """Implementation of the install command. run_callback(ctx, agent, ...) runs an instance."""
    os.environ["BYPASS_TOOL_CONSENT"] = "true"
    from dotenv import load_dotenv
    load_dotenv(override=True)
    cli_lifecycle.ensure_otel_bridge_autostart()
    cli_lifecycle.ensure_telegram_gateway_autostart()
    check_notifications()
    print_logo()

    from armorer.scanner import VulnerabilityScanner
    from armorer.installers.dynamic import DynamicInstaller

    registry = AppRegistry()
    agent_id = infer_agent_id(agent)
    if use:
        existing = get_app_with_openclaw_fallback(registry, use)
        if not existing:
            cli_common.console.print(f"[red]Error: Alias '{use}' not found.[/red]")
            if agent_id == "openclaw":
                openclaw_aliases = sorted({str(app.name).strip() for app in instances_for_agent(registry, "openclaw") if str(app.name).strip()})
                if openclaw_aliases:
                    cli_common.console.print(
                        "[dim]Installed OpenClaw aliases: " + ", ".join(openclaw_aliases) + ".[/dim]"
                    )
                else:
                    cli_common.console.print(
                        "[dim]OpenClaw is not installed for this environment. Ask Armorer chat/TUI to "
                        "`install openclaw`, or run `armorer install openclaw --yes`.[/dim]"
                    )
            raise SystemExit(1)
        if yes or not sys.stdin.isatty():
            run_callback(ctx, use, mode="background")
        else:
            run_callback(ctx, use)
        return

    existing_instances = instances_for_agent(registry, agent)
    instance_name = (alias or "").strip() if alias else ""
    if instance_name:
        existing_alias = get_app_with_openclaw_fallback(registry, instance_name)
        if existing_alias:
            if can_reinstall_stale_default_openclaw_alias(registry, agent, instance_name):
                cli_common.console.print(
                    "[dim]Existing `openclaw` registry metadata points to a missing runtime. Reinstalling it in place.[/dim]"
                )
            else:
                cli_common.console.print(f"[red]Error: Alias '{instance_name}' already exists.[/red]")
                raise SystemExit(1)

    if existing_instances and not instance_name:
        default_alias = next_instance_alias(registry, agent_id)
        interactive = (not yes) and sys.stdin.isatty()
        if interactive:
            choices = ["Run existing instance", "Install new instance"]
            action = cli_common.ui_select(
                f"An instance of '{agent_id}' already exists. What do you want to do?",
                choices=choices,
                default="Run existing instance",
            )
            if action == "Run existing instance":
                pick_choices = [app.name for app in existing_instances]
                selected = cli_common.ui_select("Select instance to run:", choices=pick_choices, default=pick_choices[0])
                if selected:
                    run_callback(ctx, selected)
                return
            proposed = cli_common.ui_text("Alias for new instance:", default=default_alias)
            instance_name = (proposed or default_alias).strip()
            if get_app_with_openclaw_fallback(registry, instance_name):
                cli_common.console.print(f"[red]Error: Alias '{instance_name}' already exists.[/red]")
                raise SystemExit(1)
        else:
            instance_name = default_alias
            cli_common.console.print(f"[dim]Existing '{agent_id}' instance detected. Using alias '{instance_name}'.[/dim]")

    if not instance_name:
        instance_name = agent_id or agent

    # Always perform a security scan unless explicitly skipped via --skip-scan.
    should_scan = not skip_scan

    if should_scan:
        with cli_common.console.status("[bold cyan]🛡️  Scanning for vulnerabilities..."):
            scanner = VulnerabilityScanner()
            scan_result = scanner.scan()
        if scan_result and scan_result.issues:
            scanner.render_report(scan_result, agent)
            if not yes:
                if not ui_confirm("Vulnerabilities found. Do you still want to proceed?", default=False):
                    return

    profile = RiskProfile(
        name="AGENTIC_DEFAULT",
        description="Standard profile for assisted installation",
        network_policy=NetworkPolicy.OPEN,
        filesystem_policy=FilesystemPolicy.SCOPED,
    )

    installer = DynamicInstaller(
        app_name=agent,
        profile=profile,
        mount_paths=[os.getcwd()],
        options={"confirm": not yes, "run_after_install": True, "instance_name": instance_name},
    )
    installer.install()
    app_name = installer.instance_name

    cli_common.console.print()
    cli_common.console.print(Panel(
        f"[bold green]✅ {app_name}[/bold green] installed successfully!",
        border_style="green",
    ))

    if yes:
        compose_cwd = getattr(installer, "_docker_compose_cwd", installer.install_dir)
        install_dir = cli_registry.resolve_install_path(compose_cwd)
        if is_profile_only_compose(install_dir):
            cli_common.console.print(Panel(f"Agent [bold]{app_name}[/bold] is ready (profile-only compose).", border_style="green"))
        elif not getattr(installer, "_did_autostart_after_install", False):
            cli_common.console.print(Panel(f"Agent [bold]{app_name}[/bold] is installed and ready to run.", border_style="green"))
        else:
            cli_common.console.print(Panel(f"Agent [bold]{app_name}[/bold] is running in the background.", border_style="green"))
    else:
        registry_view = AppRegistry()
        app_meta = get_app_with_openclaw_fallback(registry_view, app_name)
        setup_cmd = first_setup_run_command(app_meta.run_commands if app_meta else None)
        setup_available = bool(setup_cmd)
        if setup_available:
            # OpenClaw special case: run managed setup immediately after install,
            # without an extra interactive confirmation prompt.
            if infer_agent_id(app_name) == "openclaw":
                rc = run_armorer_managed_openclaw_setup(app_name)
                if rc != 0:
                    raise SystemExit(rc)
                return

            do_setup = ui_confirm(
                f"Would you like to run initial setup for {app_name} now?", default=True
            )
            if do_setup:
                setup_args = str(setup_cmd.get("command", "") or "").split()
                if setup_args:
                    original_args = list(ctx.args)
                    try:
                        ctx.args = setup_args
                        run_callback(ctx, app_name)
                    finally:
                        ctx.args = original_args
                else:
                    run_callback(ctx, app_name)
            else:
                launch = ui_confirm(f"Would you like to launch {app_name} now?", default=True)
                if launch:
                    run_callback(ctx, app_name)
        else:
            launch = ui_confirm(f"Would you like to launch {app_name} now?", default=True)
            if launch:
                run_callback(ctx, app_name)


def list_apps_impl(*, is_ignorable_cli_exit_fn: Callable[[str, str], bool]) -> None:
    """Implementation of the list command."""
    with cli_common.console.status("[bold cyan]Loading installed agents...[/bold cyan]"):
        registry = AppRegistry()
        apps = cli_registry.list_apps_with_openclaw_fallback(registry)

    if not apps:
        cli_common.console.print("[yellow]No agents installed yet.[/yellow]  Run [bold]armorer install <agent>[/bold] to get started.")
        return

    with cli_common.console.status("[bold cyan]Checking Docker container status...[/bold cyan]"):
        container_statuses: dict = {}
        try:
            result = subprocess.run(
                ["docker", "ps", "-a", "--format", "{{.Names}}\t{{.Status}}"],
                capture_output=True, text=True,
            )
            for line in (result.stdout or "").strip().splitlines():
                if "\t" in line:
                    name, status = line.split("\t", 1)
                    container_statuses[name] = status
        except Exception:
            pass

    table = Table(title="🛡️  Installed Agents", border_style="cyan", show_lines=True)
    table.add_column("Alias", style="bold white", no_wrap=True)
    table.add_column("Agent", style="cyan", no_wrap=True)
    table.add_column("Status", no_wrap=True)
    table.add_column("Health", no_wrap=True)
    table.add_column("Path", style="dim")

    with cli_common.console.status("[bold cyan]Building agent status table...[/bold cyan]"):
        for app_meta in apps:
            agent_statuses = []
            agent_health = []
            for cname, cstatus in container_statuses.items():
                if cname.startswith(f"{app_meta.name}-") or cname == app_meta.name:
                    if is_ignorable_cli_exit_fn(cname, cstatus):
                        continue
                    agent_statuses.append(cstatus)
                    lower = cstatus.lower()
                    if "unhealthy" in lower:
                        agent_health.append("unhealthy")
                    elif "healthy" in lower:
                        agent_health.append("healthy")
                    elif "health: starting" in lower or "starting" in lower:
                        agent_health.append("starting")

            install_path = cli_registry.resolve_install_path(app_meta.install_path)
            if not agent_statuses:
                try:
                    res = docker_runner.compose_ps(
                        cwd=install_path,
                        args=["--format", "json"],
                        timeout=5,
                    )
                    if res.returncode == 0 and (res.stdout or "").strip():
                        parsed_entries = []
                        raw = (res.stdout or "").strip()
                        try:
                            parsed = json.loads(raw)
                            if isinstance(parsed, list):
                                parsed_entries = [p for p in parsed if isinstance(p, dict)]
                            elif isinstance(parsed, dict):
                                parsed_entries = [parsed]
                        except json.JSONDecodeError:
                            for line in raw.splitlines():
                                try:
                                    info = json.loads(line)
                                    if isinstance(info, dict):
                                        parsed_entries.append(info)
                                except json.JSONDecodeError:
                                    continue
                        for info in parsed_entries:
                            state = info.get("State", info.get("status", ""))
                            health = info.get("Health", "")
                            if state:
                                agent_statuses.append(state)
                            if isinstance(health, str) and health:
                                agent_health.append(health.lower())
                except Exception:
                    pass

            if not agent_statuses:
                status_str = "[dim]○ Not running[/dim]"
            elif all("running" in s.lower() or "up" in s.lower() for s in agent_statuses):
                status_str = f"[green]● Running[/green]  [dim]({len(agent_statuses)} container{'s' if len(agent_statuses) > 1 else ''})[/dim]"
            elif any("running" in s.lower() or "up" in s.lower() for s in agent_statuses):
                running = sum(1 for s in agent_statuses if "running" in s.lower() or "up" in s.lower())
                status_str = f"[yellow]◐ Partial[/yellow]  [dim]({running}/{len(agent_statuses)} up)[/dim]"
            else:
                status_str = f"[red]○ Stopped[/red]  [dim]({len(agent_statuses)} container{'s' if len(agent_statuses) > 1 else ''})[/dim]"

            running_count = sum(1 for s in agent_statuses if "running" in s.lower() or "up" in s.lower())
            if not agent_statuses:
                health_str = "[dim]-[/dim]"
            elif any("unhealthy" in h for h in agent_health):
                health_str = "[red]Unhealthy[/red]"
            elif any("starting" in h for h in agent_health):
                health_str = "[yellow]Starting[/yellow]"
            elif running_count == 0:
                health_str = "[red]Down[/red]"
            elif running_count < len(agent_statuses):
                health_str = "[yellow]Degraded[/yellow]"
            elif any("healthy" in h for h in agent_health):
                health_str = "[green]Healthy[/green]"
            else:
                health_str = "[green]Running[/green]"

            table.add_row(
                app_meta.name,
                (app_meta.agent_id or app_meta.name),
                status_str,
                health_str,
                str(install_path),
            )

    cli_common.console.print()
    cli_common.console.print(table)
    cli_common.console.print()


def stop_impl(agent: str) -> None:
    """Implementation of the stop command."""
    registry = AppRegistry()
    app = cli_registry.get_app_with_openclaw_fallback(registry, agent)
    if not app:
        matches = cli_registry.instances_for_agent(registry, agent)
        if len(matches) == 1:
            app = matches[0]
        elif len(matches) > 1:
            if sys.stdin.isatty():
                import questionary
                selected = questionary.select(
                    f"Multiple '{cli_registry.infer_agent_id(agent)}' instances found. Select one to stop:",
                    choices=[m.name for m in matches],
                ).ask()
                if selected:
                    app = cli_registry.get_app_with_openclaw_fallback(registry, selected)
            else:
                names = ", ".join(m.name for m in matches)
                cli_common.console.print(f"[red]Error: Multiple instances found for '{agent}': {names}. Use an alias.[/red]")
                raise SystemExit(1)
        if not app:
            cli_registry.print_agent_not_found(agent)
            raise SystemExit(1)
    install_dir = cli_registry.resolve_install_path(app.install_path)
    if not install_dir.exists():
        cli_common.console.print(f"[red]Error: Install path no longer exists:[/red] {install_dir}")
        raise SystemExit(1)
    try:
        res = docker_runner.compose_stop(cwd=install_dir)
    except Exception as exc:
        detail = str(exc).strip()
        cli_common.console.print(f"[yellow]Failed to stop services:[/yellow] {detail[:600]}")
        if cli_common.docker_cli_missing(detail):
            cli_common.console.print(
                "[dim]Docker CLI is required for this command. Install Docker and ensure `docker` is on PATH, then retry.[/dim]"
            )
        elif cli_common.docker_daemon_unavailable(detail):
            cli_common.console.print(
                "[dim]Docker daemon access is required for this command. Start Docker Desktop or grant access to /var/run/docker.sock, then retry.[/dim]"
            )
        raise SystemExit(1)

    if res.returncode != 0:
        detail = ((res.stderr or "") + "\n" + (res.stdout or "")).strip()
        if not detail:
            detail = f"docker compose stop exited with status {res.returncode}"
        cli_common.console.print(f"[yellow]Failed to stop services:[/yellow] {detail[:600]}")
        if cli_common.docker_cli_missing(detail):
            cli_common.console.print(
                "[dim]Docker CLI is required for this command. Install Docker and ensure `docker` is on PATH, then retry.[/dim]"
            )
        elif cli_common.docker_daemon_unavailable(detail):
            cli_common.console.print(
                "[dim]Docker daemon access is required for this command. Start Docker Desktop or grant access to /var/run/docker.sock, then retry.[/dim]"
            )
        raise SystemExit(res.returncode or 1)


def uninstall_impl(agent: str, force: bool) -> None:
    """Implementation of the uninstall command."""
    import questionary
    import shutil
    registry = AppRegistry()
    app = cli_registry.get_app_with_openclaw_fallback(registry, agent)
    if not app:
        matches = cli_registry.instances_for_agent(registry, agent)
        if len(matches) == 1:
            app = matches[0]
        elif len(matches) > 1:
            if sys.stdin.isatty():
                selected = questionary.select(
                    f"Multiple '{cli_registry.infer_agent_id(agent)}' instances found. Select one to uninstall:",
                    choices=[m.name for m in matches],
                ).ask()
                if selected:
                    app = cli_registry.get_app_with_openclaw_fallback(registry, selected)
            else:
                names = ", ".join(m.name for m in matches)
                cli_common.console.print(f"[red]Error: Multiple instances found for '{agent}': {names}. Use an alias.[/red]")
                raise SystemExit(1)
        if not app:
            cli_registry.print_agent_not_found(agent)
            raise SystemExit(1)

    if not force and not questionary.confirm(f"Delete {app.name} and all its data?").ask():
        return

    install_dir = cli_registry.resolve_install_path(app.install_path)

    if install_dir.exists():
        cli_common.console.print(f"[dim]Stopping containers in {install_dir}...[/dim]")
        try:
            res = docker_runner.compose_down(cwd=install_dir, volumes=True)
        except Exception as exc:
            detail = str(exc).strip()
            cli_common.console.print(f"[yellow]docker compose down failed:[/yellow] {detail[:600]}")
            if cli_common.docker_cli_missing(detail):
                cli_common.console.print(
                    "[dim]Docker CLI is required for this command. Install Docker and ensure `docker` is on PATH, then retry.[/dim]"
                )
            elif cli_common.docker_daemon_unavailable(detail):
                cli_common.console.print(
                    "[dim]Docker daemon access is required for this command. Start Docker Desktop or grant access to /var/run/docker.sock, then retry.[/dim]"
                )
            # Synthesize a failure-like result for downstream handling.
            from armorer.docker_runner import DockerResult as _DR
            res = _DR(args=["docker", "compose", "down", "-v"], returncode=1, stdout="", stderr=detail)

        if res.returncode != 0:
            detail = ((res.stderr or "") + "\n" + (res.stdout or "")).strip()
            if detail:
                cli_common.console.print(f"[yellow]docker compose down failed:[/yellow] {detail[:600]}")
            if cli_common.docker_cli_missing(detail):
                cli_common.console.print(
                    "[dim]Docker CLI is required for this command. Install Docker and ensure `docker` is on PATH, then retry.[/dim]"
                )
            elif cli_common.docker_daemon_unavailable(detail):
                cli_common.console.print(
                    "[dim]Docker daemon access is required for this command. Start Docker Desktop or grant access to /var/run/docker.sock, then retry.[/dim]"
                )
            cli_common.console.print("[bold red]❌ 'docker compose down' failed.[/bold red]")
            if not force:
                cli_common.console.print("[yellow]Aborting uninstall to prevent orphaned containers.[/yellow]")
                cli_common.console.print("Run with [bold]--force[/bold] to delete files anyway and attempt forced cleanup.")
                raise SystemExit(1)
            else:
                cli_common.console.print("[yellow]⚠ Forcing uninstall despite errors...[/yellow]")

    try:
        orphans = []
        ps_res = docker_runner.docker_ps(
            args=[
                "-a",
                "--format",
                '{{.ID}}\t{{.Names}}\t{{.Label "com.docker.compose.project.working_dir"}}\t{{.Label "com.docker.compose.project"}}',
            ]
        )
        if ps_res.ok and ps_res.stdout:
            for line in ps_res.stdout.splitlines():
                parts = line.split("\t")
                if len(parts) >= 4:
                    cid, cname, working_dir, project = parts
                    if str(install_dir) in working_dir:
                        orphans.append((cid, cname))
                    elif project == app.name or (getattr(app, "agent_id") and str(app.agent_id).lower() == "autogpt" and project == "autogpt_platform"):
                        orphans.append((cid, cname))

        if orphans:
            cli_common.console.print(f"[yellow]Found {len(orphans)} orphaned containers:[/yellow]")
            for cid, cname in orphans:
                cli_common.console.print(f"  - {cname} ({cid})")
            if force or questionary.confirm("Remove these orphaned containers?").ask():
                ids = [o[0] for o in orphans]
                docker_runner.docker_rm(ids)
                cli_common.console.print("[green]✓ Orphaned containers removed.[/green]")
    except Exception as e:
        cli_common.console.print(f"[yellow]⚠ Warning: Backup cleanup failed: {e}[/yellow]")

    if install_dir.exists():
        shutil.rmtree(install_dir, ignore_errors=True)
        cli_common.console.print(f"[dim]Removed {install_dir}[/dim]")

    registry.remove_app(app.name)
    cli_common.console.print(f"[green]✓ {app.name} uninstalled successfully.[/green]")


def cleanup_impl(pattern: str, force: bool) -> None:
    """Find and remove orphaned containers matching a pattern."""
    import questionary
    cli_common.console.print(f"[dim]Searching for containers matching '{pattern}'...[/dim]")
    try:
        orphans = []
        ps_res = docker_runner.docker_ps(
            args=[
                "-a",
                "--format",
                '{{.ID}}\t{{.Names}}\t{{.Label "com.docker.compose.project.working_dir"}}\t{{.Label "com.docker.compose.project"}}',
            ]
        )
        if ps_res.ok and ps_res.stdout:
            for line in ps_res.stdout.splitlines():
                parts = line.split("\t")
                if len(parts) >= 4:
                    cid, cname, working_dir, project = parts
                    if (pattern.lower() in cname.lower() or
                        pattern.lower() in (project or "").lower() or
                        pattern.lower() in (working_dir or "").lower()):
                        orphans.append((cid, cname, project or ""))

        if not orphans:
            cli_common.console.print(f"[yellow]No containers found matching '{pattern}'[/yellow]")
            return

        cli_common.console.print(f"[yellow]Found {len(orphans)} matching containers:[/yellow]")
        for cid, cname, project in orphans:
            p_label = f" [dim](project: {project})[/dim]" if project else ""
            cli_common.console.print(f"  - {cname} ({cid}){p_label}")
        if force or questionary.confirm("Remove these containers?").ask():
            ids = [o[0] for o in orphans]
            docker_runner.docker_rm(ids)
            cli_common.console.print("[green]✓ Containers removed.[/green]")
    except Exception as e:
        cli_common.console.print(f"[red]Error during cleanup: {e}[/red]")
        raise SystemExit(1)


def scan_impl(agent: str) -> None:
    """Perform a deep security audit on an installed agent."""
    import questionary
    registry = AppRegistry()
    app_meta = cli_registry.get_app_with_openclaw_fallback(registry, agent)
    if not app_meta:
        matches = cli_registry.instances_for_agent(registry, agent)
        if len(matches) == 1:
            app_meta = matches[0]
        elif len(matches) > 1:
            if sys.stdin.isatty():
                selected = questionary.select(
                    f"Multiple '{cli_registry.infer_agent_id(agent)}' instances found. Select one to scan:",
                    choices=[m.name for m in matches],
                ).ask()
                if selected:
                    app_meta = cli_registry.get_app_with_openclaw_fallback(registry, selected)
            else:
                names = ", ".join(m.name for m in matches)
                cli_common.console.print(f"[red]Error: Multiple instances found for '{agent}': {names}. Use an alias.[/red]")
                raise SystemExit(1)
        if not app_meta:
            cli_registry.print_agent_not_found(agent)
            raise SystemExit(1)

    from armorer.scanner import VulnerabilityScanner
    install_dir = cli_registry.resolve_install_path(app_meta.install_path)
    scanner = VulnerabilityScanner(target_dir=str(install_dir))
    with cli_common.console.status(f"[bold cyan]🛡️  Scanning {agent}..."):
        result = scanner.scan()
    scanner.render_report(result, agent)
