from __future__ import annotations

import json
import re
import secrets
import subprocess
import time
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional

from armorer import docker_runner

from armorer.openclaw_config import (
    OPENCLAW_RUNTIME_IMAGE_DEFAULT,
    resolve_openclaw_runtime_image,
    resolve_openclaw_runtime_image_from_compose_dir,
)


def openclaw_gateway_exec_override(
    agent: str,
    service_name: Optional[str],
    run_args: List[str],
    *,
    infer_agent_id: Callable[[str], str],
    is_read_only_check: Callable[[List[str]], bool],
) -> Optional[List[str]]:
    if infer_agent_id(agent) != "openclaw":
        return None
    if str(service_name or "").strip() != "openclaw-cli":
        return None
    if not run_args:
        return None
    head = str(run_args[0]).strip().lower()
    if head not in {"status", "doctor", "health", "gateway"}:
        return None
    if not is_read_only_check(run_args):
        return None
    return ["docker", "compose", "exec", "-T", "openclaw-gateway", "node", "dist/index.js"] + run_args


def _normalize_openclaw_config_command(row: Dict[str, Any]) -> Dict[str, Any]:
    normalized = dict(row)
    label = str(normalized.get("label") or "").strip().lower()
    command = str(normalized.get("command") or "").strip().lower()
    if label == "config" or command == "configure":
        normalized["label"] = "OpenClaw Config"
        normalized["category"] = "setup"
        normalized["description"] = "OpenClaw's own in-container configuration flow."
    return normalized


def normalize_openclaw_run_commands(
    run_commands: Optional[List[Dict[str, Any]]],
    *,
    known_run_commands: Optional[List[Dict[str, Any]]] = None,
) -> List[Dict[str, Any]]:
    existing: List[Dict[str, Any]] = [dict(row) for row in run_commands or []]
    known = [dict(row) for row in (known_run_commands or []) if row.get("command") and row.get("service")]

    # Normalize existing rows first so catalog-driven updates keep the same label
    # conventions and keep required OpenClaw config affordance intact.
    existing = [_normalize_openclaw_config_command(row) for row in existing]

    def _key(row: Dict[str, Any]) -> tuple[str, str, str]:
        return (
            str(row.get("service") or "").strip().lower(),
            str(row.get("command") or "").strip().lower(),
            str(row.get("label") or "").strip().lower(),
        )

    if known:
        index = {_key(row): idx for idx, row in enumerate(existing)}
        merged = list(existing)
        for row in known:
            normalized = _normalize_openclaw_config_command(row)
            key = _key(normalized)
            target = index.get(key)
            if target is None:
                index[key] = len(merged)
                merged.append(normalized)
            else:
                merged[target] = normalized

        # Stable dedupe keeps first-seen order while allowing catalog updates to
        # replace matching entries.
        deduped: List[Dict[str, Any]] = []
        seen: set[tuple[str, str, str]] = set()
        for row in merged:
            key = _key(row)
            if key in seen:
                continue
            seen.add(key)
            deduped.append(row)
        existing = deduped

    if existing and not any(str(cmd.get("armorer_action") or "").strip() == "app_configure" for cmd in existing):
        existing.insert(
            0,
            {
                "label": "Armorer Config",
                "service": "armorer",
                "command": "armorer-configure",
                "category": "setup",
                "description": "Recommended: configure models, channels, skills, and secrets through Armorer.",
                "armorer_action": "app_configure",
            },
        )

    return existing


def find_openclaw_shortcut_match(
    ctx_args: List[str],
    run_commands: List[Dict[str, Any]],
) -> Optional[tuple[Dict[str, Any], List[str]]]:
    if not run_commands:
        return None
    if not ctx_args:
        return None

    first_arg = str(ctx_args[0]).strip().lower()
    shortcut_match: Optional[Dict[str, Any]] = None
    matched_len = 0
    matched_priority = -1
    run_args: List[str] = []
    raw_args_lower = [str(arg).strip().lower() for arg in ctx_args]

    for cmd in run_commands:
        label = str(cmd.get("label", "")).strip()
        label_lower = label.lower()
        label_words = [token for token in re.split(r"[^a-z0-9]+", label_lower) if token]
        label_token = "".join(label_words)
        configured = [part for part in str(cmd.get("command", "")).strip().split() if part]
        configured_lower = [part.lower() for part in configured]

        candidate_args: Optional[List[str]] = None
        candidate_len = 0
        candidate_priority = -1

        if configured_lower and raw_args_lower[: len(configured_lower)] == configured_lower:
            candidate_args = list(ctx_args)
            candidate_len = len(configured_lower)
            candidate_priority = 3
        elif label_words and raw_args_lower[: len(label_words)] == label_words:
            candidate_args = configured + list(ctx_args[len(label_words):])
            candidate_len = len(label_words)
            candidate_priority = 2
        elif configured_lower and first_arg == configured_lower[0]:
            candidate_args = list(ctx_args)
            candidate_len = 1
            candidate_priority = 1
        elif first_arg == label_lower or first_arg == label_token:
            candidate_args = configured + list(ctx_args[1:])
            candidate_len = 1
            candidate_priority = 0

        if candidate_args is None:
            continue
        if candidate_len > matched_len or (candidate_len == matched_len and candidate_priority > matched_priority):
            shortcut_match = cmd
            run_args = candidate_args
            matched_len = candidate_len
            matched_priority = candidate_priority

    if shortcut_match is None:
        return None
    return shortcut_match, run_args


def prepare_openclaw_shortcut_plan(
    *,
    shortcut: Dict[str, Any],
    run_args: List[str],
    resolved_agent_ref: str,
    is_openclaw_agent: bool,
    compose_dir: Path,
    startup_services: List[str],
    startup_timeout: int,
    run_env: Dict[str, str],
    openclaw_is_read_only_check: Callable[[List[str]], bool],
    should_ensure_prereq_services: Callable[[List[str], Optional[str]], bool],
    ensure_startup_services_ready: Callable[..., bool],
    wait_openclaw_gateway_reachable: Callable[[Path, Dict[str, str], int], bool],
    openclaw_gateway_exec_override: Callable[[str, Optional[str], List[str]], Optional[List[str]]],
    is_setup_run_command: bool = False,
) -> Dict[str, Any]:
    service_name = shortcut.get("service")
    read_only_check = is_openclaw_agent and openclaw_is_read_only_check(run_args)
    prereq_services = list(startup_services or [])
    ensured_prereqs = False
    gateway_ready_for_exec = False

    gateway_exec_candidate = (
        is_openclaw_agent
        and str(service_name or "").strip() in {"openclaw-cli", "openclaw-gateway"}
        and openclaw_is_read_only_check(run_args)
    )
    if should_ensure_prereq_services(prereq_services, service_name):
        if not ensure_startup_services_ready(
            compose_dir,
            prereq_services,
            timeout_seconds=min(startup_timeout, 180),
            runtime_env=run_env,
        ):
            return {
                "abort": True,
                "rc": 1,
                "run_cmd": [
                    "docker",
                    "compose",
                    "run",
                    "--rm",
                    "--service-ports",
                ],
                "command_timeout": None,
                "is_setup_run_command": is_setup_run_command,
            }
        ensured_prereqs = True

    if ensured_prereqs and gateway_exec_candidate:
        gateway_ready_for_exec = wait_openclaw_gateway_reachable(
            compose_dir,
            run_env,
            timeout_seconds=min(startup_timeout, 120),
        )
        if not gateway_ready_for_exec:
            if bool(getattr(wait_openclaw_gateway_reachable, "last_failure_is_fatal", False)):
                return {
                    "abort": True,
                    "abort_message": "[yellow]OpenClaw gateway probe hit a fatal Docker error; aborting run instead of fallback execution.[/yellow]",
                    "abort_exit": True,
                    "rc": 1,
                    "run_cmd": [
                        "docker",
                        "compose",
                        "run",
                        "--rm",
                        "--service-ports",
                    ],
                    "command_timeout": None,
                    "is_setup_run_command": is_setup_run_command,
                }

    use_no_deps = ensured_prereqs or is_setup_run_command
    fallback_with_deps = False
    if ensured_prereqs and gateway_exec_candidate and not gateway_ready_for_exec:
        use_no_deps = False
        fallback_with_deps = True

    run_cmd = ["docker", "compose", "run", "--rm", "--service-ports"]
    if use_no_deps:
        run_cmd.append("--no-deps")
    run_cmd.extend([service_name] + run_args)

    exec_override = (
        openclaw_gateway_exec_override(resolved_agent_ref, service_name, run_args)
        if ensured_prereqs and gateway_exec_candidate and gateway_ready_for_exec
        else None
    )
    if exec_override:
        run_cmd = exec_override

    command_timeout = None
    if is_openclaw_agent and openclaw_is_read_only_check(run_args):
        command_timeout = max(30, min(startup_timeout, 180))

    return {
        "abort": False,
        "run_cmd": run_cmd,
        "command_timeout": command_timeout,
        "service_name": service_name,
        "run_args": run_args,
        "is_setup_run_command": is_setup_run_command,
        "fallback_with_deps": fallback_with_deps,
        "exec_override_used": bool(exec_override),
        "read_only_check": read_only_check,
        "label": shortcut.get("label", ""),
    }


def run_openclaw_shortcut_command(
    *,
    run_cmd: List[str],
    compose_dir: Path,
    run_env: Dict[str, str],
    command_timeout: Optional[int],
    run_args: List[str],
    resolved_agent_ref: str,
    shortcut: Dict[str, Any],
    startup_timeout: int,
    instance_alias: Optional[str],
    console: Any,
    run_process: Callable[..., Any],
    subprocess_stdio_kwargs: Dict[str, Any],
    maybe_offer_openclaw_gateway_background: Callable[..., bool],
    safe_apply_openclaw_setup_defaults: Callable[[str, str, Path, Dict[str, str]], None],
    compose_dir_for_display: Optional[Path],
    docker_cli_missing: Callable[[str], bool],
    docker_daemon_unavailable: Callable[[str], bool],
    show_fallback_with_deps: bool = False,
) -> Dict[str, Any]:
    shortcut_label = str(shortcut.get("label", "")).strip()
    if show_fallback_with_deps:
        console.print(
            "[dim]Falling back to compose run with dependencies because OpenClaw gateway probe was not reachable.[/dim]"
        )
    try:
        result = run_process(
            run_cmd,
            cwd=compose_dir,
            env=run_env,
            timeout=command_timeout,
            **subprocess_stdio_kwargs,
        )
        rc = int(result.returncode)
    except KeyboardInterrupt:
        rc = 130
    except subprocess.TimeoutExpired as exc:
        timeout_value = int(exc.timeout) if exc.timeout is not None else int(command_timeout or 0)
        console.print(f"[yellow]Command timed out after {timeout_value}s:[/yellow] {' '.join(run_cmd)}")
        rc = 1
    except Exception as exc:
        detail = str(exc).strip()
        console.print(f"[yellow]Failed to run command:[/yellow] {detail[:600]}")
        if docker_cli_missing(detail):
            console.print(
                "[dim]Docker CLI is required for this command. Install Docker and ensure `docker` is on PATH, then retry.[/dim]"
            )
        elif docker_daemon_unavailable(detail):
            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]"
            )
        rc = 1

    if maybe_offer_openclaw_gateway_background(
        resolved_agent_ref,
        shortcut,
        run_args,
        rc,
        compose_dir_for_display or compose_dir,
        startup_timeout,
        instance_alias=instance_alias,
    ):
        return {"rc": rc, "handled": True}

    if rc == 0:
        safe_apply_openclaw_setup_defaults(resolved_agent_ref, shortcut_label, compose_dir, run_env)

    return {"rc": rc, "handled": False}


def execute_openclaw_shortcut(
    *,
    shortcut: Dict[str, Any],
    run_args: List[str],
    resolved_agent_ref: str,
    compose_dir: Path,
    install_dir: Path,
    startup_timeout: int,
    startup_services: List[str],
    runtime_env_for_execution: Callable[[bool], Dict[str, str]],
    prefill_agent: str,
    instance_alias: Optional[str],
    console: Any,
    openclaw_is_read_only_check: Callable[[List[str]], bool],
    should_ensure_prereq_services: Callable[[List[str], Optional[str]], bool],
    ensure_startup_services_ready: Callable[..., bool],
    wait_openclaw_gateway_reachable: Callable[[Path, Dict[str, str], int], bool],
    openclaw_gateway_exec_override: Callable[[str, Optional[str], List[str]], Optional[List[str]]],
    prefill_from_vault_checklist: Callable[[str, Path, str, List[str]], None],
    run_process: Callable[..., Any],
    subprocess_stdio_kwargs: Dict[str, Any],
    maybe_offer_openclaw_gateway_background: Callable[..., bool],
    safe_apply_openclaw_setup_defaults: Callable[[str, str, Path, Dict[str, str]], None],
    docker_cli_missing: Callable[[str], bool],
    docker_daemon_unavailable: Callable[[str], bool],
    is_setup_run_command: bool = False,
) -> int:
    service_name = shortcut.get("service")
    read_only_check = openclaw_is_read_only_check(run_args)
    run_env = runtime_env_for_execution(read_only_check)
    prereq_services = list(startup_services or [])

    if should_ensure_prereq_services(prereq_services, service_name):
        console.print(f"[dim]Ensuring prerequisite services are ready: {', '.join(prereq_services)}[/dim]")

    plan = prepare_openclaw_shortcut_plan(
        shortcut=shortcut,
        run_args=run_args,
        resolved_agent_ref=resolved_agent_ref,
        is_openclaw_agent=True,
        compose_dir=compose_dir,
        startup_services=prereq_services,
        startup_timeout=startup_timeout,
        run_env=run_env,
        openclaw_is_read_only_check=openclaw_is_read_only_check,
        should_ensure_prereq_services=should_ensure_prereq_services,
        ensure_startup_services_ready=ensure_startup_services_ready,
        wait_openclaw_gateway_reachable=wait_openclaw_gateway_reachable,
        openclaw_gateway_exec_override=openclaw_gateway_exec_override,
        is_setup_run_command=is_setup_run_command,
    )
    if plan.get("abort"):
        message = str(plan.get("abort_message") or "").strip()
        if message:
            console.print(message, soft_wrap=True)
        return int(plan.get("rc", 1))
    if plan.get("fallback_with_deps"):
        console.print("[yellow]OpenClaw gateway did not report reachable in time; continuing anyway.[/yellow]")

    prefill_from_vault_checklist(prefill_agent, install_dir, str(shortcut.get("label", "")), run_args)
    console.print(
        f"\n[cyan]▶ Armorer Running Shortcut: docker compose run --rm {service_name} {' '.join(run_args)}[/cyan]"
    )
    if plan.get("exec_override_used"):
        console.print(f"[dim]Using gateway exec path for OpenClaw: {' '.join(plan['run_cmd'])}[/dim]")

    result = run_openclaw_shortcut_command(
        run_cmd=plan["run_cmd"],
        compose_dir=compose_dir,
        run_env=run_env,
        command_timeout=plan.get("command_timeout"),
        run_args=run_args,
        resolved_agent_ref=resolved_agent_ref,
        shortcut=shortcut,
        startup_timeout=startup_timeout,
        instance_alias=instance_alias,
        console=console,
        run_process=run_process,
        subprocess_stdio_kwargs=subprocess_stdio_kwargs,
        maybe_offer_openclaw_gateway_background=maybe_offer_openclaw_gateway_background,
        safe_apply_openclaw_setup_defaults=safe_apply_openclaw_setup_defaults,
        compose_dir_for_display=compose_dir,
        docker_cli_missing=docker_cli_missing,
        docker_daemon_unavailable=docker_daemon_unavailable,
        show_fallback_with_deps=bool(plan.get("fallback_with_deps")),
    )
    if result.get("handled"):
        return 0
    return int(result.get("rc", 1))


def preflight_interactive_custom_openclaw_run(
    compose_dir: Path,
    *,
    console: Any,
    run_process: Callable[..., Any],
    docker_cli_missing: Callable[[str], bool],
    docker_daemon_unavailable: Callable[[str], bool],
) -> Optional[int]:
    def _warn_preflight_failure(prefix: str, detail: str) -> None:
        clean_detail = str(detail or "").strip()
        if clean_detail:
            console.print(f"[yellow]{prefix}:[/yellow] {clean_detail[:600]}")
        else:
            console.print(f"[yellow]{prefix}.[/yellow]")
        if docker_cli_missing(clean_detail):
            console.print(
                "[dim]Docker CLI is required for this command. Install Docker and ensure `docker` is on PATH, then retry.[/dim]"
            )
        elif docker_daemon_unavailable(clean_detail):
            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]"
            )

    try:
        inspect_result = docker_runner.compose_ps(cwd=compose_dir, args=["-q"])
    except Exception as exc:
        _warn_preflight_failure("Failed to inspect running services", str(exc).strip())
        return 1

    if inspect_result.returncode != 0:
        detail = ((inspect_result.stderr or "") + "\n" + (inspect_result.stdout or "")).strip()
        if not detail:
            detail = f"docker compose ps -q exited with status {inspect_result.returncode}"
        _warn_preflight_failure("Failed to inspect running services", detail)
        return int(inspect_result.returncode or 1)

    if not str(inspect_result.stdout or "").strip():
        return None

    console.print("[dim]Stopping background services to free ports...[/dim]")
    try:
        stop_result = docker_runner.compose_down(cwd=compose_dir)
    except Exception as exc:
        _warn_preflight_failure("Failed to stop background services", str(exc).strip())
        return None

    if stop_result.returncode != 0:
        detail = ((stop_result.stderr or "") + "\n" + (stop_result.stdout or "")).strip()
        if not detail:
            detail = f"docker compose down exited with status {stop_result.returncode}"
        _warn_preflight_failure("Failed to stop background services", detail)
    return None


def openclaw_is_read_only_check(run_args: List[str]) -> bool:
    if not run_args:
        return False
    head = str(run_args[0]).strip().lower()
    if head in {"status", "doctor", "health"}:
        return True
    if head == "gateway" and len(run_args) > 1:
        sub = str(run_args[1]).strip().lower()
        return sub in {"probe", "status", "health"}
    return False


def wait_openclaw_gateway_reachable(
    compose_dir: Path,
    run_env: Dict[str, str],
    *,
    console: Any,
    docker_cli_missing: Callable[[str], bool],
    docker_daemon_unavailable: Callable[[str], bool],
    timeout_seconds: int = 60,
) -> bool:
    wait_openclaw_gateway_reachable.last_failure_is_fatal = False  # type: ignore[attr-defined]

    def _report_probe_failure(detail: str) -> None:
        clean_detail = str(detail or "").strip()
        if clean_detail:
            console.print(f"[yellow]OpenClaw gateway probe failed:[/yellow] {clean_detail[:600]}")
        else:
            console.print("[yellow]OpenClaw gateway probe failed.[/yellow]")
        if docker_cli_missing(clean_detail):
            console.print(
                "[dim]Docker CLI is required for this command. Install Docker and ensure `docker` is on PATH, then retry.[/dim]"
            )
        elif docker_daemon_unavailable(clean_detail):
            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]"
            )

    wait_budget = max(10, min(int(timeout_seconds), 75))
    deadline = time.time() + wait_budget
    probe_cmd = ["node", "dist/index.js", "gateway", "probe"]
    while time.time() < deadline:
        remaining = max(1, int(deadline - time.time()))
        probe_timeout = min(8, remaining)
        try:
            res = docker_runner.compose_exec(
                cwd=compose_dir,
                service="openclaw-gateway",
                exec_args=probe_cmd,
                env=run_env,
                timeout=probe_timeout,
            )
        except subprocess.TimeoutExpired:
            time.sleep(1)
            continue
        except Exception as exc:
            detail = str(exc).strip()
            if docker_daemon_unavailable(detail) or docker_cli_missing(detail):
                wait_openclaw_gateway_reachable.last_failure_is_fatal = True  # type: ignore[attr-defined]
                _report_probe_failure(detail)
                return False
            time.sleep(1)
            continue
        out = ((res.stdout or "") + "\n" + (res.stderr or "")).strip()
        low_out = out.lower()
        if "reachable: yes" in low_out:
            return True
        if docker_daemon_unavailable(low_out) or docker_cli_missing(low_out):
            wait_openclaw_gateway_reachable.last_failure_is_fatal = True  # type: ignore[attr-defined]
            _report_probe_failure(out)
            return False
        time.sleep(2)
    return False


def is_openclaw_gateway_action(
    agent: str,
    cmd_meta: Dict[str, Any],
    run_args: List[str],
    *,
    infer_agent_id: Callable[[str], str],
) -> bool:
    if infer_agent_id(agent) != "openclaw":
        return False
    label = str(cmd_meta.get("label", "") or "").strip().lower()
    command = str(cmd_meta.get("command", "") or "").strip().lower()
    first_arg = str(run_args[0] if run_args else "").strip().lower()
    command_head = command.split()[0] if command else ""
    return label == "gateway" or first_arg == "gateway" or command_head == "gateway"


def openclaw_stop_hint(alias: Optional[str] = None) -> str:
    target = str(alias or "").strip() or "openclaw"
    return f"`armorer stop {target}`"


def handle_openclaw_gateway_interrupt(
    compose_dir: Path,
    startup_timeout: int,
    *,
    console: Any,
    stdin_isatty: bool,
    ui_confirm: Callable[[str, bool], bool],
    build_runtime_env: Callable[[Path], Dict[str, str]],
    ensure_startup_services_ready: Callable[..., bool],
    instance_alias: Optional[str] = None,
) -> None:
    stop_hint = openclaw_stop_hint(instance_alias)
    console.print("\n[yellow]Gateway run interrupted.[/yellow]")
    if not stdin_isatty:
        console.print(f"[dim]Run {stop_hint} to stop services explicitly.[/dim]")
        return
    keep_running = ui_confirm("Keep OpenClaw gateway running in background?", default=True)
    if keep_running:
        run_env = build_runtime_env(compose_dir)
        ok = ensure_startup_services_ready(
            compose_dir,
            ["openclaw-gateway"],
            timeout_seconds=min(startup_timeout, 180),
            runtime_env=run_env,
        )
        if ok:
            console.print("[green]✓ OpenClaw gateway is running in background.[/green]")
        else:
            console.print("[yellow]Gateway was not confirmed ready in background.[/yellow]")
    console.print(f"[dim]To stop it intentionally: {stop_hint}[/dim]")


def maybe_offer_openclaw_gateway_background(
    agent: str,
    cmd_meta: Dict[str, Any],
    run_args: List[str],
    rc: int,
    compose_dir: Path,
    startup_timeout: int,
    *,
    infer_agent_id: Callable[[str], str],
    console: Any,
    stdin_isatty: bool,
    ui_confirm: Callable[[str, bool], bool],
    build_runtime_env: Callable[[Path], Dict[str, str]],
    ensure_startup_services_ready: Callable[..., bool],
    instance_alias: Optional[str] = None,
) -> bool:
    if not is_openclaw_gateway_action(agent, cmd_meta, run_args, infer_agent_id=infer_agent_id):
        return False
    if rc not in (0, 130, 137):
        return False
    if not stdin_isatty:
        return False
    stop_hint = openclaw_stop_hint(instance_alias)
    keep_running = ui_confirm(
        f"Keep OpenClaw gateway running in background? Use {stop_hint} to stop it.",
        default=True,
    )
    if keep_running:
        run_env = build_runtime_env(compose_dir)
        ok = ensure_startup_services_ready(
            compose_dir,
            ["openclaw-gateway"],
            timeout_seconds=min(startup_timeout, 180),
            runtime_env=run_env,
        )
        if ok:
            console.print("[green]✓ OpenClaw gateway is running in background.[/green]")
        else:
            console.print("[yellow]Gateway was not confirmed ready in background.[/yellow]")
    console.print(f"[dim]To stop it intentionally: {stop_hint}[/dim]")
    return True


def maybe_apply_openclaw_setup_defaults(
    agent: str,
    setup_label: str,
    compose_dir: Path,
    run_env: Dict[str, str],
    *,
    infer_agent_id: Callable[[str], str],
    console: Any,
    stdin_isatty: bool,
    ui_text: Callable[[str, Optional[str]], Optional[str]],
    ui_confirm: Callable[[str, bool], bool],
    panel_factory: Callable[..., Any],
    docker_cli_missing: Callable[[str], bool],
    docker_daemon_unavailable: Callable[[str], bool],
    ensure_startup_services_ready: Callable[..., bool],
) -> None:
    post_setup_timeout = 60
    if infer_agent_id(agent) != "openclaw":
        return
    label = str(setup_label or "").lower()
    if not any(k in label for k in ("setup", "configure", "onboard")):
        return

    def _cfg_get(path: str) -> str:
        res = docker_runner.compose_run(
            cwd=compose_dir,
            service="openclaw-cli",
            run_args=("config", "get", path),
            env=run_env,
            timeout=post_setup_timeout,
        )
        stdout_text = (res.stdout or "").strip()
        stderr_text = (res.stderr or "").strip()
        out = ((res.stdout or "") + "\n" + (res.stderr or "")).strip()
        if res.returncode != 0:
            low_out = out.lower()
            if "config path not found" in low_out:
                return out
            detail = out or f"docker compose config get exited with status {res.returncode}"
            raise RuntimeError(f"OpenClaw config get failed ({path}): {detail[:700]}")
        if stdout_text:
            return stdout_text
        if not stderr_text:
            return ""
        filtered_lines = []
        for line in stderr_text.splitlines():
            low_line = line.strip().lower()
            if not low_line:
                continue
            if low_line.startswith("warn"):
                continue
            if low_line.startswith("time=") and "level=warning" in low_line:
                continue
            filtered_lines.append(line.strip())
        return "\n".join(filtered_lines).strip()

    def _is_empty_allow(raw: str) -> bool:
        text = (raw or "").strip().lower()
        if not text or "config path not found" in text:
            return True
        compact = re.sub(r"\s+", "", text)
        return compact in {"[]", "null", '""', "''"}

    def _cfg_set(path: str, value: str, strict_json: bool = False) -> bool:
        cmd = ["config", "set", path, value]
        if strict_json:
            cmd.append("--strict-json")
        res = docker_runner.compose_run(
            cwd=compose_dir,
            service="openclaw-cli",
            run_args=cmd,
            env=run_env,
            timeout=post_setup_timeout,
        )
        if res.returncode == 0:
            return True
        detail = ((res.stdout or "") + "\n" + (res.stderr or "")).strip()
        if "config path not found" in detail.lower():
            return False
        if not detail:
            detail = f"docker compose config set exited with status {res.returncode}"
        raise RuntimeError(f"OpenClaw config set failed ({path}): {detail[:700]}")

    def _looks_set(value: str) -> bool:
        text = str(value or "").strip()
        if not text:
            return False
        lower = text.lower()
        if "config path not found" in lower or lower in {"null", '""', "''"}:
            return False
        return True

    tg_token_cfg = _cfg_get("channels.telegram.botToken")
    tg_token_env = ""
    try:
        from dotenv import dotenv_values
        env_values = dotenv_values(compose_dir / ".env")
        tg_token_env = str(env_values.get("TELEGRAM_BOT_TOKEN") or "")
    except Exception:
        tg_token_env = ""
    tg_token_runtime = str(run_env.get("TELEGRAM_BOT_TOKEN", "") or "")
    if not any(_looks_set(v) for v in (tg_token_cfg, tg_token_env, tg_token_runtime)):
        return

    policy = _cfg_get("channels.telegram.groupPolicy").strip().lower()
    if policy == "allowlist":
        group_allow = _cfg_get("channels.telegram.groupAllowFrom")
        allow_from = _cfg_get("channels.telegram.allowFrom")
        if _is_empty_allow(group_allow) and _is_empty_allow(allow_from):
            ids: List[int] = []
            if stdin_isatty:
                console.print(
                    panel_factory(
                        "[bold yellow]Telegram Access Setup[/bold yellow]\n"
                        "Telegram group policy is currently [bold]allowlist[/bold], but no allowed user IDs are set.\n"
                        "Provide your Telegram numeric user ID(s) to avoid dropped messages.\n"
                        "Use comma-separated values for multiple IDs (example: 123456789, 987654321).",
                        border_style="yellow",
                        expand=False,
                    )
                )
                raw_ids = ui_text("Telegram user ID(s) (blank to skip):", "") or ""
                for token in re.split(r"[\s,]+", raw_ids.strip()):
                    if token.isdigit():
                        ids.append(int(token))
                ids = list(dict.fromkeys(ids))

            if ids:
                payload = json.dumps(ids)
                ok_allow = _cfg_set("channels.telegram.allowFrom", payload, strict_json=True)
                ok_group = _cfg_set("channels.telegram.groupAllowFrom", payload, strict_json=True)
                if ok_allow or ok_group:
                    console.print(f"[green]✓ Configured Telegram allowlist IDs:[/green] {', '.join(str(x) for x in ids)}")
            elif _cfg_set("channels.telegram.groupPolicy", "open"):
                console.print("[dim]Applied OpenClaw setup default: channels.telegram.groupPolicy=open (no allowlist IDs provided).[/dim]")

    if stdin_isatty:
        console.print(
            panel_factory(
                "[bold cyan]Telegram Pairing[/bold cyan]\n"
                "If you enabled Telegram in setup, send a pairing request from Telegram to the bot,\n"
                "then paste the pairing code here to approve it immediately.\n\n"
                "Token checks:\n"
                "• `grep -E \"^TELEGRAM_BOT_TOKEN=\" .env`\n"
                "• `docker compose run --rm -T openclaw-cli config get channels.telegram.botToken`",
                border_style="cyan",
                expand=False,
            )
        )
        pairing_code = ui_text("Telegram pairing code (blank to skip):", "")
        code = re.sub(r"\s+", "", str(pairing_code or "")).upper()
        if code:
            pair_res = docker_runner.compose_run(
                cwd=compose_dir,
                service="openclaw-cli",
                run_args=("pairing", "approve", "telegram", code),
                env=run_env,
                timeout=post_setup_timeout,
            )
            if pair_res.returncode == 0:
                console.print(f"[green]✓ Approved Telegram pairing code {code}.[/green]")
                if ui_confirm("Start OpenClaw gateway now so Telegram chat is live?", default=True):
                    if ensure_startup_services_ready(
                        compose_dir,
                        ["openclaw-gateway"],
                        timeout_seconds=120,
                        runtime_env=run_env,
                    ):
                        console.print("[green]✓ OpenClaw gateway is up. You can chat from Telegram now.[/green]")
                    else:
                        console.print(
                            "[yellow]Gateway did not become ready. Run this instance in background mode "
                            "(for example, `armorer run <alias> --mode background`) and check logs.[/yellow]"
                        )
            else:
                detail = ((pair_res.stderr or "") + "\n" + (pair_res.stdout or "")).strip() or f"exit code {pair_res.returncode}"
                console.print(f"[yellow]Telegram pairing approval failed:[/yellow] {detail[:600]}")
                if docker_cli_missing(detail):
                    console.print("[dim]Docker CLI is required for this command. Install Docker and ensure `docker` is on PATH, then retry.[/dim]")
                elif docker_daemon_unavailable(detail):
                    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]")


def safe_apply_openclaw_setup_defaults(
    agent: str,
    setup_label: str,
    compose_dir: Path,
    run_env: Dict[str, str],
    **kwargs: Any,
) -> None:
    console = kwargs["console"]
    docker_cli_missing = kwargs["docker_cli_missing"]
    docker_daemon_unavailable = kwargs["docker_daemon_unavailable"]
    try:
        maybe_apply_openclaw_setup_defaults(agent, setup_label, compose_dir, run_env, **kwargs)
    except Exception as exc:
        detail = str(exc).strip() or exc.__class__.__name__
        console.print(f"[yellow]Skipping post-setup defaults:[/yellow] {detail[:600]}")
        if docker_cli_missing(detail):
            console.print("[dim]Docker CLI is required for this command. Install Docker and ensure `docker` is on PATH, then retry.[/dim]")
        elif docker_daemon_unavailable(detail):
            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]")


def reconcile_openclaw_gateway_token(
    compose_dir: Path,
    run_env: Dict[str, str],
    *,
    console: Any,
) -> None:
    env_path = compose_dir / ".env"
    compose_yml = compose_dir / "docker-compose.yml"
    compose_yaml = compose_dir / "docker-compose.yaml"
    if not compose_yml.exists() and not compose_yaml.exists():
        return

    if env_path.exists():
        try:
            raw_lines = env_path.read_text(encoding="utf-8", errors="ignore").splitlines()
            cleaned: List[str] = []
            changed = False
            for line in raw_lines:
                stripped = line.strip()
                if not stripped or stripped.startswith("#") or re.match(r"^\s*[A-Za-z_][A-Za-z0-9_]*\s*=.*$", line):
                    cleaned.append(line)
                    continue
                cleaned.append(f"# [armorer-sanitized-invalid-env-line] {stripped}")
                changed = True
            if changed:
                env_path.write_text("\n".join(cleaned).rstrip() + "\n", encoding="utf-8")
                console.print("[yellow]Sanitized malformed lines in OpenClaw .env before token reconciliation.[/yellow]")
        except Exception:
            pass

    def _valid_token(value: str) -> bool:
        text = str(value or "").strip()
        if not re.match(r"^[A-Za-z0-9._:-]{16,}$", text):
            return False
        lowered = text.lower()
        blocked_markers = ("redacted", "change-me", "changeme", "replace-me", "placeholder", "example", "dummy")
        return not any(marker in lowered for marker in blocked_markers)

    def _update_env_var(path: Path, key: str, value: str) -> bool:
        if not path.exists():
            return False
        raw_lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
        out_lines: List[str] = []
        replaced = False
        for line in raw_lines:
            if re.match(rf"^\s*{re.escape(key)}\s*=", line):
                out_lines.append(f"{key}={value}")
                replaced = True
            else:
                out_lines.append(line)
        if not replaced:
            out_lines.append(f"{key}={value}")
        path.write_text("\n".join(out_lines).rstrip() + "\n", encoding="utf-8")
        return True

    env_token = ""
    try:
        for line in env_path.read_text(encoding="utf-8", errors="ignore").splitlines():
            if re.match(r"^\s*OPENCLAW_GATEWAY_TOKEN\s*=", line):
                env_token = line.split("=", 1)[1].strip().strip('"').strip("'")
        if not _valid_token(env_token):
            env_token = ""
    except Exception:
        env_token = ""

    cfg_path = compose_dir / ".openclaw" / "openclaw.json"
    cfg_data: Dict[str, Any] = {}
    cfg_token = ""
    mode = ""
    try:
        if cfg_path.exists():
            loaded = json.loads(cfg_path.read_text(encoding="utf-8", errors="ignore") or "{}")
            if isinstance(loaded, dict):
                cfg_data = loaded
    except Exception:
        cfg_data = {}
    gateway_cfg = cfg_data.get("gateway", {}) if isinstance(cfg_data.get("gateway"), dict) else {}
    auth_cfg = gateway_cfg.get("auth", {}) if isinstance(gateway_cfg.get("auth"), dict) else {}
    remote_cfg = gateway_cfg.get("remote", {}) if isinstance(gateway_cfg.get("remote"), dict) else {}
    maybe_cfg_token = str(auth_cfg.get("token") or "").strip()
    if _valid_token(maybe_cfg_token):
        cfg_token = maybe_cfg_token
    mode = str(auth_cfg.get("mode") or "").strip().lower()

    chosen = cfg_token or env_token or secrets.token_hex(32)
    changed = False
    if str(gateway_cfg.get("mode") or "").strip().lower() != "local":
        gateway_cfg["mode"] = "local"
        changed = True
    if mode != "token":
        auth_cfg["mode"] = "token"
        changed = True
    if str(auth_cfg.get("token") or "").strip() != chosen:
        auth_cfg["token"] = chosen
        changed = True
    if str(remote_cfg.get("token") or "").strip() != chosen:
        remote_cfg["token"] = chosen
        changed = True
    gateway_cfg["auth"] = auth_cfg
    gateway_cfg["remote"] = remote_cfg
    cfg_data["gateway"] = gateway_cfg
    if changed:
        try:
            cfg_path.parent.mkdir(parents=True, exist_ok=True)
            cfg_path.write_text(json.dumps(cfg_data, indent=2) + "\n", encoding="utf-8")
        except Exception:
            pass

    if env_path.exists() and env_token != chosen:
        changed = _update_env_var(env_path, "OPENCLAW_GATEWAY_TOKEN", chosen) or changed

    run_env["OPENCLAW_GATEWAY_TOKEN"] = chosen
    if changed:
        console.print("[dim]OpenClaw gateway token reconciled (env + config).[/dim]")


def ensure_openclaw_runtime_defaults(
    compose_dir: Path,
    run_env: Dict[str, str],
    *,
    console: Any,
    persist: bool = True,
) -> None:
    env_path = compose_dir / ".env"
    image = resolve_openclaw_runtime_image_from_compose_dir(compose_dir, run_env)
    if not image:
        image = OPENCLAW_RUNTIME_IMAGE_DEFAULT
        if persist and env_path.exists():
            try:
                lines = env_path.read_text(encoding="utf-8", errors="ignore").splitlines()
                out: List[str] = []
                replaced = False
                for line in lines:
                    if re.match(r"^\s*OPENCLAW_IMAGE\s*=", line):
                        out.append(f"OPENCLAW_IMAGE={image}")
                        replaced = True
                    else:
                        out.append(line)
                if not replaced:
                    out.append(f"OPENCLAW_IMAGE={image}")
                env_path.write_text("\n".join(out).rstrip() + "\n", encoding="utf-8")
            except Exception:
                pass
    run_env["OPENCLAW_IMAGE"] = image

    def _valid_token(value: str) -> bool:
        return bool(re.match(r"^[A-Za-z0-9._:-]{16,}$", str(value or "").strip()))

    cfg_token = ""
    cfg_path = compose_dir / ".openclaw" / "openclaw.json"
    try:
        if cfg_path.exists():
            cfg = json.loads(cfg_path.read_text(encoding="utf-8", errors="ignore") or "{}")
            if isinstance(cfg, dict):
                gateway = cfg.get("gateway", {}) if isinstance(cfg.get("gateway"), dict) else {}
                auth = gateway.get("auth", {}) if isinstance(gateway.get("auth"), dict) else {}
                token = str(auth.get("token") or "").strip()
                if _valid_token(token):
                    cfg_token = token
    except Exception:
        cfg_token = ""

    if cfg_token and env_path.exists() and persist:
        try:
            lines = env_path.read_text(encoding="utf-8", errors="ignore").splitlines()
            out: List[str] = []
            replaced = False
            for line in lines:
                if re.match(r"^\s*OPENCLAW_GATEWAY_TOKEN\s*=", line):
                    out.append(f"OPENCLAW_GATEWAY_TOKEN={cfg_token}")
                    replaced = True
                else:
                    out.append(line)
            if not replaced:
                out.append(f"OPENCLAW_GATEWAY_TOKEN={cfg_token}")
            env_path.write_text("\n".join(out).rstrip() + "\n", encoding="utf-8")
        except Exception:
            pass
    if cfg_token:
        run_env["OPENCLAW_GATEWAY_TOKEN"] = cfg_token


def ensure_openclaw_gateway_allowed_origins(compose_dir: Path, *, console: Any, persist: bool = True) -> None:
    env_values: Dict[str, str] = {}
    try:
        from dotenv import dotenv_values
        env_values = {k: str(v) for k, v in (dotenv_values(compose_dir / ".env") or {}).items() if k and v is not None}
    except Exception:
        env_values = {}

    bind = str(env_values.get("OPENCLAW_GATEWAY_BIND", "loopback") or "loopback").strip().lower()
    if bind == "loopback" or not persist:
        return
    port = str(env_values.get("OPENCLAW_GATEWAY_PORT", "18789") or "18789").strip() or "18789"

    cfg_dir = compose_dir / ".openclaw"
    cfg_path = cfg_dir / "openclaw.json"
    try:
        cfg_dir.mkdir(parents=True, exist_ok=True)
        data: Dict[str, Any] = {}
        if cfg_path.exists():
            try:
                data = json.loads(cfg_path.read_text(encoding="utf-8") or "{}")
            except Exception:
                data = {}
        gateway = data.setdefault("gateway", {})
        control_ui = gateway.setdefault("controlUi", {})
        origins = control_ui.get("allowedOrigins")
        if not isinstance(origins, list) or not origins:
            control_ui["allowedOrigins"] = [f"http://127.0.0.1:{port}"]
            cfg_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
            console.print("[dim]Applied OpenClaw runtime default: gateway.controlUi.allowedOrigins[/dim]")
    except Exception:
        return
