"""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.config import resolve_armorer_config_dir
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 _cleanup_uninstalled_agent_artifacts(agent_name: str) -> None:
    config_dir = resolve_armorer_config_dir()
    history_file = config_dir / "conversation_history" / f"agent:{agent_name}.jsonl"
    report_dir = config_dir / "reports"

    for path in (history_file,):
        try:
            path.unlink(missing_ok=True)
        except Exception:
            pass

    if report_dir.exists():
        for pdf in report_dir.glob(f"{agent_name}-daily-*.pdf"):
            try:
                pdf.unlink()
            except Exception:
                continue


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)
    _cleanup_uninstalled_agent_artifacts(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)
