"""Data/service helpers used by Armorer web API endpoints."""

from __future__ import annotations

import json
import os
import re
import subprocess
import time
from pathlib import Path
from typing import Any
from urllib.parse import quote, urlparse

from armorer import db as armorer_db
from armorer.catalog import all_known_agents
from armorer.cli import common as cli_common
from armorer.cli.core_lifecycle import (
    list_running_host_core_pids,
    list_running_telegram_gateway_pids,
    restart_telegram_gateway_process,
)
from armorer.cli.model_catalog import CURATED_MODELS, DEFAULT_MODELS
from armorer.cli.telegram_admin import load_recent_telegram_targets_cache
from armorer.config import ConfigManager
from armorer.core.telegram_setup import _discover_targets_from_updates, _recommended_ids_from_targets
from armorer.messaging.telegram import TelegramGateway
from armorer.registry import AppRegistry
from armorer.security.conversation_guard import ConversationGuard
from armorer.tools.app_setup import apply_openclaw_config, get_openclaw_setup_context
from armorer.apps.openclaw import (
    approve_openclaw_browser_device_pairing,
    discover_openclaw_pairing_requests,
    load_openclaw_instance_config,
    seed_openclaw_telegram_allowlist,
    verify_openclaw_channels,
)
from armorer.apps.openclaw.config_adapter import OPENCLAW_PROVIDER_ENV_KEYS
from armorer.openclaw_config import resolve_openclaw_registered_install_path
from armorer.tailscale import tailscale_serve_status_summary, tailscale_status_summary
from armorer.tools.orchestrator import list_agents_structured


_SECRET_PATTERNS = (
    re.compile(r"(api[_-]?key\s*[=:]\s*)([^\s,;]+)", re.IGNORECASE),
    re.compile(r"(token\s*[=:]\s*)([^\s,;]+)", re.IGNORECASE),
    re.compile(r"(secret\s*[=:]\s*)([^\s,;]+)", re.IGNORECASE),
    re.compile(r"(password\s*[=:]\s*)([^\s,;]+)", re.IGNORECASE),
)


def _redact_text(text: str) -> str:
    out = str(text or "")
    for patt in _SECRET_PATTERNS:
        out = patt.sub(r"\1***REDACTED***", out)
    return out


def sanitize_job_payload(payload: dict[str, Any]) -> dict[str, Any]:
    safe = dict(payload)
    if isinstance(safe.get("message"), str):
        safe["message"] = _redact_text(safe["message"])
    if isinstance(safe.get("recent_output"), list):
        safe["recent_output"] = [_redact_text(str(line)) for line in safe["recent_output"][-80:]]
    result = safe.get("result")
    if isinstance(result, dict):
        result_copy = dict(result)
        if isinstance(result_copy.get("stdout_tail"), str):
            result_copy["stdout_tail"] = _redact_text(result_copy["stdout_tail"])
        safe["result"] = result_copy
    return safe


def read_recent_jobs(limit: int = 30) -> list[dict[str, Any]]:
    primary = Path.home() / ".armorer" / "jobs"
    fallback = Path.home() / ".armorer" / "jobs-local"
    jobs_dir = primary if primary.exists() and os.access(primary, os.R_OK | os.X_OK) else fallback
    if not jobs_dir.exists():
        return []
    out: list[dict[str, Any]] = []
    paths = sorted(jobs_dir.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
    for path in paths[: max(1, int(limit))]:
        try:
            raw = json.loads(path.read_text(encoding="utf-8"))
        except Exception:
            continue
        if not isinstance(raw, dict):
            continue
        out.append(
            sanitize_job_payload(
                {
                    "job_id": raw.get("job_id", path.stem),
                    "type": raw.get("type", ""),
                    "status": raw.get("status", ""),
                    "phase": raw.get("phase", ""),
                    "message": raw.get("message", ""),
                    "updated_at": raw.get("updated_at"),
                    "started_at": raw.get("started_at"),
                    "heartbeat_at": raw.get("heartbeat_at"),
                }
            )
        )
    return out


def runtime_events_summary(*, limit: int = 100, agent: str = "", event_type: str = "") -> dict[str, Any]:
    config_dir = cli_common.armorer_config_dir()
    items = armorer_db.list_runtime_events(
        config_dir,
        limit=limit,
        agent=agent,
        event_type=event_type,
    )
    return {
        "ok": True,
        "items": items,
    }


def status_summary() -> dict[str, Any]:
    cm = ConfigManager()
    provider = (cm.get_key("MODEL_PROVIDER") or "").strip().lower()
    model = (cm.get_key("Armorer_MODEL") or "").strip()
    guard_settings = _load_guard_settings(Path(cm.CONFIG_DIR))
    # region agent log
    try:
        _logp = Path(__file__).resolve().parents[3] / ".cursor" / "debug-047d00.log"
        _line = json.dumps(
            {
                "sessionId": "047d00",
                "runId": "ui-setup-gate",
                "hypothesisId": "H3",
                "location": "services.py:status_summary",
                "message": "status computed",
                "data": {
                    "provider": provider,
                    "model_present": bool(model),
                },
                "timestamp": int(__import__("time").time() * 1000),
            },
            ensure_ascii=False,
        )
        with open(_logp, "a", encoding="utf-8") as _f:
            _f.write(_line + "\n")
    except Exception:
        pass
    # endregion

    key_map = {
        "openai": "OPENAI_API_KEY",
        "anthropic": "ANTHROPIC_API_KEY",
        "gemini": "GEMINI_API_KEY",
        "openrouter": "OPENROUTER_API_KEY",
        "bedrock": "BEDROCK_API_KEY",
    }
    needed = key_map.get(provider, "")
    has_key = True if not needed else bool((cm.get_key(needed) or "").strip())

    docker_enabled = cli_common.probe_docker_daemon(detail_limit=80) is None
    core_running = core_service_running()
    tg_running = bool(list_running_telegram_gateway_pids())
    telegram_enabled = _bool_setting(cm.get_key("ARMORER_GATEWAY_TELEGRAM_ENABLED"), default=True)
    armorer_telegram_token = str(cm.get_armorer_telegram_bot_token() or "").strip()
    telegram_bot_token = bool(armorer_telegram_token)
    telegram_bot_identity = _telegram_bot_identity(armorer_telegram_token)
    telegram_alert_chat_id = str(guard_settings.get("telegram_alert_chat_id") or cm.get_key("TELEGRAM_ALERT_CHAT_ID") or "").strip()
    telegram_admin_chat_ids = list(guard_settings.get("telegram_admin_chat_ids", []) or _split_list(cm.get_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS")))
    telegram_admin_user_ids = list(guard_settings.get("telegram_admin_user_ids", []) or _split_list(cm.get_key("ARMORER_TELEGRAM_ADMIN_USER_IDS")))
    telegram_has_routing = bool(telegram_alert_chat_id or telegram_admin_chat_ids or telegram_admin_user_ids)
    telegram_setup_complete = bool(telegram_enabled and telegram_bot_token and telegram_has_routing)

    return {
        "ok": True,
        "runtime_mode": "docker-enabled" if docker_enabled else "local-only",
        "llm": {
            "provider": provider,
            "model": model,
            "configured": bool(provider),
            "credential_present": has_key,
            "required_key": needed,
        },
        "core_running": core_running,
        "telegram_gateway_running": tg_running,
        "telegram": {
            "enabled": telegram_enabled,
            "bot_token_configured": telegram_bot_token,
            "bot_handle": telegram_bot_identity.get("handle", ""),
            "bot_username": telegram_bot_identity.get("username", ""),
            "bot_name": telegram_bot_identity.get("name", ""),
            "routing_configured": telegram_has_routing,
            "setup_complete": telegram_setup_complete,
        },
    }


def core_service_running() -> bool:
    if list_running_host_core_pids():
        return True
    try:
        result = subprocess.run(
            [
                "docker",
                "ps",
                "--filter",
                "name=armorer-core",
                "--filter",
                "status=running",
                "--format",
                "{{.Names}}",
            ],
            capture_output=True,
            text=True,
        )
    except Exception:
        return False
    if result.returncode != 0:
        return False
    names = [str(line or "").strip() for line in (result.stdout or "").splitlines()]
    return "armorer-core" in names


def _autoconfigure_telegram_from_recent_updates(
    *,
    cm: ConfigManager,
    guard_settings: dict[str, Any],
    token: str,
) -> bool:
    token = str(token or "").strip()
    if not token:
        return False

    existing_alert_chat_id = str(guard_settings.get("telegram_alert_chat_id") or cm.get_key("TELEGRAM_ALERT_CHAT_ID") or "").strip()
    existing_admin_chat_ids = list(guard_settings.get("telegram_admin_chat_ids", []) or _split_list(cm.get_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS")))
    existing_admin_user_ids = list(guard_settings.get("telegram_admin_user_ids", []) or _split_list(cm.get_key("ARMORER_TELEGRAM_ADMIN_USER_IDS")))
    if existing_alert_chat_id or existing_admin_chat_ids or existing_admin_user_ids:
        return True

    try:
        gateway = TelegramGateway(bot_token=token)
        targets = _discover_targets_from_updates(gateway)
    except Exception:
        targets = []
    if not targets:
        try:
            targets = load_recent_telegram_targets_cache()
        except Exception:
            targets = []

    admin_ids, alert_chat = _recommended_ids_from_targets(targets)
    if not admin_ids:
        return False

    csv = ",".join(admin_ids)
    cm.set_key("ARMORER_TELEGRAM_ADMIN_USER_IDS", csv)
    cm.set_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS", csv)
    cm.set_key("ARMORER_TELEGRAM_ALLOW_UNCONFIGURED_ADMINS", "false")
    guard_settings["telegram_admin_user_ids"] = list(admin_ids)
    guard_settings["telegram_admin_chat_ids"] = list(admin_ids)
    guard_settings["telegram_allow_unconfigured_admins"] = False
    if alert_chat:
        cm.set_key("TELEGRAM_ALERT_CHAT_ID", alert_chat)
        guard_settings["telegram_alert_chat_id"] = alert_chat
    return True


def _validate_telegram_bot_token(token: str) -> tuple[bool, str, dict[str, Any]]:
    token = str(token or "").strip()
    if not token:
        return False, "Telegram bot token is missing.", {}
    try:
        bot = TelegramGateway(bot_token=token).get_me()
    except Exception as exc:
        return False, f"Armorer could not verify that Telegram bot token. {exc}", {}


def _telegram_bot_identity(token: str) -> dict[str, str]:
    raw_token = str(token or "").strip()
    if not raw_token:
        return {}
    try:
        gateway = TelegramGateway(bot_token=raw_token)
        bot = gateway.get_me() or {}
        username = str(bot.get("username") or "").strip()
        first_name = str(bot.get("first_name") or "").strip()
        try:
            gateway.close()
        except Exception:
            pass
        payload: dict[str, str] = {}
        if username:
            payload["username"] = username
            payload["handle"] = f"@{username}"
        if first_name:
            payload["name"] = first_name
        return payload
    except Exception:
        return {}
    if not isinstance(bot, dict) or not bot.get("id"):
        return False, "Telegram accepted the request, but did not return valid bot details.", {}
    username = str(bot.get("username") or "").strip()
    display_name = str(bot.get("first_name") or username or "Telegram bot").strip()
    message = f"Connected to Telegram bot {display_name}."
    if username:
        message = f"Connected to Telegram bot @{username}."
    return True, message, bot


def alerts_summary(*, limit: int = 50, sort: str = "criticality", status: str = "", agent: str = "") -> dict[str, Any]:
    config_dir = cli_common.armorer_config_dir()
    settings_path = config_dir / "guard_settings.json"

    settings: dict[str, Any] = {}
    try:
        if settings_path.exists():
            parsed = json.loads(settings_path.read_text(encoding="utf-8"))
            if isinstance(parsed, dict):
                settings = parsed
    except Exception:
        settings = {}

    db_summary = armorer_db.security_alerts_summary(config_dir)
    items = armorer_db.list_security_alerts(
        config_dir,
        limit=limit,
        sort=sort,
        status=status,
        agent=agent,
    )

    return {
        "ok": True,
        "alerting_enabled": bool(settings.get("alerting_enabled", True)),
        "proactive_security_enabled": bool(settings.get("proactive_security_enabled", False)),
        "telegram_alert_chat_id": str(settings.get("telegram_alert_chat_id") or "").strip(),
        "telegram_admin_chat_ids": list(settings.get("telegram_admin_chat_ids", []) or []),
        "telegram_admin_user_ids": list(settings.get("telegram_admin_user_ids", []) or []),
        "telegram_allow_unconfigured_admins": bool(settings.get("telegram_allow_unconfigured_admins", False)),
        "recent_alert_count": int(db_summary.get("open_alert_count", 0)),
        "latest_alert_at": db_summary.get("latest_alert_at"),
        "items": items,
    }


def _load_guard_settings(config_dir: Path) -> dict[str, Any]:
    settings_path = config_dir / "guard_settings.json"
    try:
        if settings_path.exists():
            parsed = json.loads(settings_path.read_text(encoding="utf-8"))
            if isinstance(parsed, dict):
                return parsed
    except Exception:
        pass
    return {}


def _setting_value(value: Any) -> Any:
    if value is None:
        return ""
    if isinstance(value, (str, int, float, bool, list, dict)):
        return value
    return str(value)


def _bool_setting(value: Any, *, default: bool = False) -> bool:
    if value is None or value == "":
        return default
    if isinstance(value, bool):
        return value
    return str(value).strip().lower() not in {"0", "false", "off", "no", ""}


def _split_list(value: Any) -> list[str]:
    if isinstance(value, list):
        raw = value
    else:
        raw = re.split(r"[\n,]+", str(value or ""))
    out: list[str] = []
    for item in raw:
        text = str(item or "").strip()
        if text and text not in out:
            out.append(text)
    return out


def _structured_settings_payload(cm: ConfigManager, non_secret: list[dict[str, Any]], secrets: list[dict[str, Any]], guard_settings: dict[str, Any]) -> dict[str, Any]:
    provider = str(cm.get_key("MODEL_PROVIDER") or "").strip().lower()
    model = str(cm.get_key("Armorer_MODEL") or "").strip()
    provider_key_map = {
        "openai": "OPENAI_API_KEY",
        "anthropic": "ANTHROPIC_API_KEY",
        "gemini": "GEMINI_API_KEY",
        "openrouter": "OPENROUTER_API_KEY",
        "bedrock": "BEDROCK_API_KEY",
    }
    required_key = provider_key_map.get(provider, "")
    model_choices = list(CURATED_MODELS.get(provider, []))
    default_model = str(DEFAULT_MODELS.get(provider) or "").strip()
    if model and model not in model_choices:
        model_choices.insert(0, model)
    if default_model and default_model not in model_choices:
        model_choices.append(default_model)
    provider_model_choices = {
        provider_name: list(CURATED_MODELS.get(provider_name, []))
        for provider_name in provider_key_map
    }
    provider_model_choices["ollama"] = list(CURATED_MODELS.get("ollama", []))
    provider_default_models = {
        provider_name: str(DEFAULT_MODELS.get(provider_name) or "").strip()
        for provider_name in {**provider_key_map, "ollama": None}
    }
    telegram_alert_chat_id = str(guard_settings.get("telegram_alert_chat_id") or cm.get_key("TELEGRAM_ALERT_CHAT_ID") or "").strip()
    telegram_admin_chat_ids = list(guard_settings.get("telegram_admin_chat_ids", []) or _split_list(cm.get_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS")))
    telegram_admin_user_ids = list(guard_settings.get("telegram_admin_user_ids", []) or _split_list(cm.get_key("ARMORER_TELEGRAM_ADMIN_USER_IDS")))
    telegram_allow_unconfigured_admins = bool(
        guard_settings.get(
            "telegram_allow_unconfigured_admins",
            _bool_setting(cm.get_key("ARMORER_TELEGRAM_ALLOW_UNCONFIGURED_ADMINS"), default=False),
        )
    )
    telegram_bot_identity = _telegram_bot_identity(str(cm.get_armorer_telegram_bot_token() or "").strip())
    telegram_shortcuts = _telegram_shortcuts_state(str(cm.get_armorer_telegram_bot_token() or "").strip())
    openclaw_apps = [
        app
        for app in AppRegistry().list_apps()
        if str(app.agent_id or app.name or "").strip().lower() == "openclaw"
    ]
    tailscale_section = None
    for app in openclaw_apps:
        install_dir = resolve_openclaw_registered_install_path(str(app.install_path or ""))
        current = load_openclaw_instance_config(
            install_dir,
            app.app_config_state if isinstance(app.app_config_state, dict) else {},
        )
        if str(current.access_mode or "local").strip().lower() == "remote_tailscale":
            tailscale_section = {
                "visible": True,
                **_openclaw_tailscale_state(current),
            }
            break

    return {
        "llm": {
            "provider": provider,
            "model": model,
            "ollama_base_url": cm.get_key("OLLAMA_BASE_URL") or "",
            "required_key": required_key,
            "credential_present": bool(cm.get_key(required_key)) if required_key else True,
            "model_choices": model_choices,
            "provider_model_choices": provider_model_choices,
            "provider_default_models": provider_default_models,
            "credentials": {
                "OPENAI_API_KEY": bool(cm.get_key("OPENAI_API_KEY")),
                "ANTHROPIC_API_KEY": bool(cm.get_key("ANTHROPIC_API_KEY")),
                "GEMINI_API_KEY": bool(cm.get_key("GEMINI_API_KEY")),
                "OPENROUTER_API_KEY": bool(cm.get_key("OPENROUTER_API_KEY")),
                "BEDROCK_API_KEY": bool(cm.get_key("BEDROCK_API_KEY")),
            },
        },
        "daily_reports": {
            "enabled": _bool_setting(cm.get_key("ARMORER_DAILY_REPORT_ENABLED"), default=True),
            "time": cm.get_key("ARMORER_DAILY_REPORT_TIME") or "09:00",
            "window_hours": cm.get_key("ARMORER_DAILY_REPORT_WINDOW_HOURS") or "24",
        },
        "telemetry": {
            "collector_enabled": _bool_setting(cm.get_key("ARMORER_OTEL_COLLECTOR_ENABLED"), default=True),
            "collector_port": cm.get_key("ARMORER_OTEL_COLLECTOR_PORT") or "4318",
            "collector_image": cm.get_key("ARMORER_OTEL_COLLECTOR_IMAGE") or "otel/opentelemetry-collector-contrib:0.120.0",
            "endpoint": cm.get_key("ARMORER_OTEL_ENDPOINT") or "",
        },
        "telegram": {
            "gateway_enabled": _bool_setting(cm.get_key("ARMORER_GATEWAY_TELEGRAM_ENABLED"), default=True),
            "bot_token_configured": bool(cm.get_armorer_telegram_bot_token()),
            "bot_handle": telegram_bot_identity.get("handle", ""),
            "bot_username": telegram_bot_identity.get("username", ""),
            "bot_name": telegram_bot_identity.get("name", ""),
            "alert_chat_id": telegram_alert_chat_id,
            "admin_chat_ids": telegram_admin_chat_ids,
            "admin_user_ids": telegram_admin_user_ids,
            "allow_unconfigured_admins": telegram_allow_unconfigured_admins,
            "shortcut_commands": list(telegram_shortcuts.get("commands") or []),
            "shortcuts_synced": bool(telegram_shortcuts.get("synced")),
            "shortcuts_error": str(telegram_shortcuts.get("last_error") or "").strip(),
        },
        "security": {
            "llm_guard_enabled": bool(guard_settings.get("llm_guard_enabled", True)),
            "alerting_enabled": bool(guard_settings.get("alerting_enabled", True)),
            "proactive_security_enabled": bool(guard_settings.get("proactive_security_enabled", False)),
            "alert_confidence_threshold": guard_settings.get("alert_confidence_threshold", 0.0),
            "input_block_enabled": bool(guard_settings.get("input_block_enabled", True)),
            "input_block_confidence_threshold": guard_settings.get("input_block_confidence_threshold", 0.85),
            "block_on_flags": list(guard_settings.get("block_on_flags", []) or []),
            "disabled_alert_flags": list(guard_settings.get("disabled_alert_flags", []) or []),
            "input_scanners": dict(guard_settings.get("input_scanners", {}) or {}),
            "output_scanners": dict(guard_settings.get("output_scanners", {}) or {}),
        },
        "advanced": {
            "non_secret_settings": non_secret,
            "secret_settings": secrets,
        },
        "tailscale": tailscale_section,
    }


def settings_summary() -> dict[str, Any]:
    cm = ConfigManager()
    env_file_settings = cm.load_env()
    vault = cm._load_vault()

    non_secret_keys = set(env_file_settings.keys())
    non_secret_keys.update(
        key
        for key, value in os.environ.items()
        if value
        and not cm.is_secret(key)
        and (
            key.startswith("ARMORER_")
            or key in {"MODEL_PROVIDER", "Armorer_MODEL", "OLLAMA_BASE_URL"}
        )
    )

    non_secret = []
    for key in sorted(non_secret_keys):
        value = cm.get_key(key)
        source = "env_file" if key in env_file_settings else "environment"
        non_secret.append(
            {
                "key": key,
                "value": _setting_value(value),
                "source": source,
            }
        )

    secret_keys = set(vault.get("secrets", {}).keys())
    secret_keys.update(
        key
        for key, value in os.environ.items()
        if value
        and cm.is_secret(key)
        and (
            key.startswith("ARMORER_")
            or key.endswith("_API_KEY")
            or key in {"TELEGRAM_BOT_TOKEN"}
        )
    )

    secrets = []
    for key in sorted(secret_keys):
        meta = vault.get("keys", {}).get(key, {})
        source = "vault" if key in vault.get("secrets", {}) else "environment"
        configured = bool(cm.get_key(key))
        secrets.append(
            {
                "key": key,
                "configured": configured,
                "source": source,
                "description": str(meta.get("description") or "").strip(),
                "aliases": [str(alias or "").strip() for alias in meta.get("aliases", []) if str(alias or "").strip()],
            }
        )

    guard_settings = _load_guard_settings(Path(cm.CONFIG_DIR))
    return {
        "ok": True,
        "config_dir": str(cm.CONFIG_DIR),
        "env_path": str(cm.env_path),
        "vault_path": str(cm.VAULT_FILE),
        "non_secret_settings": non_secret,
        "secret_settings": secrets,
        "guard_settings": guard_settings,
        "sections": _structured_settings_payload(cm, non_secret, secrets, guard_settings),
    }


def reveal_secret_value(key: str) -> dict[str, Any]:
    cm = ConfigManager()
    normalized = str(key or "").strip()
    if not normalized:
        return {"ok": False, "message": "Secret key is required."}
    if not cm.is_secret(normalized):
        return {"ok": False, "message": f"{normalized} is not a secret setting."}
    value = str(cm.get_key(normalized) or "")
    return {
        "ok": True,
        "key": normalized,
        "configured": bool(value.strip()),
        "value": value,
    }


def update_settings(payload: dict[str, Any]) -> dict[str, Any]:
    cm = ConfigManager()
    config_dir = Path(cm.CONFIG_DIR)
    guard = ConversationGuard(config_dir)
    guard_settings = guard.get_guard_settings()

    llm = payload.get("llm")
    if isinstance(llm, dict):
        if "provider" in llm:
            cm.set_key("MODEL_PROVIDER", str(llm.get("provider") or "").strip().lower())
        if "model" in llm:
            cm.set_key("Armorer_MODEL", str(llm.get("model") or "").strip())
        if "ollama_base_url" in llm:
            cm.set_key("OLLAMA_BASE_URL", str(llm.get("ollama_base_url") or "").strip())
        for key_name in ("OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY", "OPENROUTER_API_KEY", "BEDROCK_API_KEY"):
            if key_name in llm:
                cm.set_key(key_name, str(llm.get(key_name) or "").strip())

    daily_reports = payload.get("daily_reports")
    if isinstance(daily_reports, dict):
        if "enabled" in daily_reports:
            cm.set_key("ARMORER_DAILY_REPORT_ENABLED", "true" if bool(daily_reports.get("enabled")) else "false")
        if "time" in daily_reports:
            cm.set_key("ARMORER_DAILY_REPORT_TIME", str(daily_reports.get("time") or "").strip())
        if "window_hours" in daily_reports:
            cm.set_key("ARMORER_DAILY_REPORT_WINDOW_HOURS", str(daily_reports.get("window_hours") or "").strip())

    telemetry = payload.get("telemetry")
    if isinstance(telemetry, dict):
        if "collector_enabled" in telemetry:
            cm.set_key("ARMORER_OTEL_COLLECTOR_ENABLED", "true" if bool(telemetry.get("collector_enabled")) else "false")
        if "collector_port" in telemetry:
            cm.set_key("ARMORER_OTEL_COLLECTOR_PORT", str(telemetry.get("collector_port") or "").strip())
        if "collector_image" in telemetry:
            cm.set_key("ARMORER_OTEL_COLLECTOR_IMAGE", str(telemetry.get("collector_image") or "").strip())
        if "endpoint" in telemetry:
            cm.set_key("ARMORER_OTEL_ENDPOINT", str(telemetry.get("endpoint") or "").strip())

    restart_gateway_requested = bool(payload.get("restartTelegramGateway"))
    telegram_restart_required = False
    validated_telegram_message = ""
    previous_telegram_enabled = _bool_setting(cm.get_key("ARMORER_GATEWAY_TELEGRAM_ENABLED"), default=True)
    previous_telegram_token = str(cm.get_armorer_telegram_bot_token() or "").strip()
    telegram = payload.get("telegram")
    if isinstance(telegram, dict):
        bot_token = ""
        if "gateway_enabled" in telegram:
            next_enabled = bool(telegram.get("gateway_enabled"))
            if next_enabled != previous_telegram_enabled:
                telegram_restart_required = True
            cm.set_key("ARMORER_GATEWAY_TELEGRAM_ENABLED", "true" if next_enabled else "false")
        if "bot_token" in telegram:
            bot_token = str(telegram.get("bot_token") or "").strip()
            if bot_token and bot_token != previous_telegram_token:
                valid_token, validated_telegram_message, _bot = _validate_telegram_bot_token(bot_token)
                if not valid_token:
                    return {"ok": False, "message": validated_telegram_message}
                telegram_restart_required = True
            cm.set_armorer_telegram_bot_token(bot_token)
        if "alert_chat_id" in telegram:
            alert_chat_id = str(telegram.get("alert_chat_id") or "").strip()
            cm.set_key("TELEGRAM_ALERT_CHAT_ID", alert_chat_id)
            guard_settings["telegram_alert_chat_id"] = alert_chat_id
        if "admin_chat_ids" in telegram:
            chat_ids = _split_list(telegram.get("admin_chat_ids"))
            cm.set_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS", ",".join(chat_ids))
            guard_settings["telegram_admin_chat_ids"] = chat_ids
        if "admin_user_ids" in telegram:
            user_ids = _split_list(telegram.get("admin_user_ids"))
            cm.set_key("ARMORER_TELEGRAM_ADMIN_USER_IDS", ",".join(user_ids))
            guard_settings["telegram_admin_user_ids"] = user_ids
        if "allow_unconfigured_admins" in telegram:
            allow = bool(telegram.get("allow_unconfigured_admins"))
            cm.set_key("ARMORER_TELEGRAM_ALLOW_UNCONFIGURED_ADMINS", "true" if allow else "false")
            guard_settings["telegram_allow_unconfigured_admins"] = allow
        if bot_token:
            _autoconfigure_telegram_from_recent_updates(cm=cm, guard_settings=guard_settings, token=bot_token)

    security = payload.get("security")
    if isinstance(security, dict):
        for key_name in (
            "llm_guard_enabled",
            "alerting_enabled",
            "proactive_security_enabled",
            "alert_confidence_threshold",
            "input_block_enabled",
            "input_block_confidence_threshold",
            "block_on_flags",
            "disabled_alert_flags",
            "input_scanners",
            "output_scanners",
        ):
            if key_name in security:
                guard_settings[key_name] = security.get(key_name)

    guard.save_guard_settings(guard_settings)
    payload_out = settings_summary()
    if restart_gateway_requested and telegram_restart_required:
        restarted, message = restart_telegram_gateway_process()
        refreshed = settings_summary()
        if validated_telegram_message:
            refreshed["message"] = f"{validated_telegram_message} {message}"
        else:
            refreshed["message"] = message
        if not restarted:
            refreshed["ok"] = False
        return refreshed
    if telegram_restart_required:
        if validated_telegram_message:
            payload_out["message"] = f"{validated_telegram_message} Restart the Telegram gateway for the change to take effect."
        else:
            payload_out["message"] = "Telegram settings were saved. Restart the Telegram gateway for the change to take effect."
    return payload_out


def sync_telegram_setup() -> dict[str, Any]:
    cm = ConfigManager()
    guard = ConversationGuard(Path(cm.CONFIG_DIR))
    guard_settings = guard.get_guard_settings()
    token = str(cm.get_armorer_telegram_bot_token() or "").strip()
    if not token:
        return {
            "ok": False,
            "message": "Telegram bot token is not configured yet.",
        }
    configured = _autoconfigure_telegram_from_recent_updates(cm=cm, guard_settings=guard_settings, token=token)
    guard.save_guard_settings(guard_settings)
    payload = settings_summary()
    if configured:
        return payload
    try:
        bot_username = str(TelegramGateway(bot_token=token).get_me().get("username") or "").strip()
    except Exception:
        bot_username = ""
    message = "No recent Telegram messages were found for this bot yet."
    if bot_username:
        message = f"No recent Telegram messages were found for @{bot_username} yet."
    payload["message"] = message + " Send a direct message to that exact bot, then press Sync now again."
    return payload


def _telegram_shortcut_commands() -> list[dict[str, str]]:
    return [
        {"command": "urls", "description": "Show Armorer and agent URLs"},
        {"command": "status", "description": "Show Armorer and agent health"},
        {"command": "help", "description": "Show supported Telegram commands"},
    ]


def _telegram_shortcuts_state(token: str) -> dict[str, Any]:
    commands = _telegram_shortcut_commands()
    token = str(token or "").strip()
    if not token:
        return {
            "supported": True,
            "synced": False,
            "commands": commands,
            "last_error": "",
        }
    gateway = None
    try:
        gateway = TelegramGateway(bot_token=token)
        configured = gateway.get_my_commands()
        normalized = sorted(
            (
                {
                    "command": str(item.get("command") or "").strip(),
                    "description": str(item.get("description") or "").strip(),
                }
                for item in configured
            ),
            key=lambda item: item["command"],
        )
        expected = sorted(commands, key=lambda item: item["command"])
        return {
            "supported": True,
            "synced": normalized == expected,
            "commands": commands,
            "last_error": "",
        }
    except Exception as exc:
        return {
            "supported": True,
            "synced": False,
            "commands": commands,
            "last_error": f"Armorer could not check Telegram command shortcuts. {exc}",
        }
    finally:
        try:
            if gateway:
                gateway.close()
        except Exception:
            pass


def sync_telegram_shortcuts() -> dict[str, Any]:
    cm = ConfigManager()
    token = str(cm.get_armorer_telegram_bot_token() or "").strip()
    if not token:
        return {
            "ok": False,
            "message": "Telegram bot token is not configured yet.",
        }

    gateway = None
    try:
        gateway = TelegramGateway(bot_token=token)
        result = gateway.set_my_commands(_telegram_shortcut_commands())
        if not result.get("ok"):
            return {
                "ok": False,
                "message": "Armorer could not sync Telegram command shortcuts.",
            }
    except Exception as exc:
        return {
            "ok": False,
            "message": f"Armorer could not sync Telegram command shortcuts. {exc}",
        }
    finally:
        try:
            if gateway:
                gateway.close()
        except Exception:
            pass

    payload = settings_summary()
    payload["message"] = "Telegram command shortcuts synced."
    return payload


def supported_agents_summary() -> dict[str, Any]:
    agents = all_known_agents()
    return {
        "ok": True,
        "count": len(agents),
        "agents": [
            {
                "id": a.id,
                "aliases": list(a.aliases or []),
                "repo_url": a.repo_url,
                "compose_subdir": a.compose_subdir,
                "startup_services": list(a.startup_services or []),
                "playbook": a.playbook,
            }
            for a in agents
        ],
    }


def _openclaw_dashboard_url(install_path: str) -> str:
    root = Path(str(install_path or "").strip())
    env_path = root / ".env"
    if not env_path.exists():
        return ""
    try:
        raw = env_path.read_text(encoding="utf-8")
    except Exception:
        return ""
    values: dict[str, str] = {}
    for line in raw.splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        values[key.strip()] = value.strip()
    port = str(values.get("OPENCLAW_GATEWAY_PORT") or "").strip()
    if not port.isdigit():
        return ""
    base_url = f"http://127.0.0.1:{port}/"

    config_path = root / ".openclaw" / "openclaw.json"
    if not config_path.exists():
        return base_url
    try:
        config = json.loads(config_path.read_text(encoding="utf-8"))
    except Exception:
        return base_url
    gateway = config.get("gateway") if isinstance(config, dict) else {}
    if not isinstance(gateway, dict):
        return base_url
    auth = gateway.get("auth")
    if not isinstance(auth, dict):
        return base_url
    mode = str(auth.get("mode") or "").strip().lower()
    token = str(auth.get("token") or "").strip()
    if mode == "token" and token:
        return f"{base_url}#token={token}"
    return base_url


def _openclaw_tailscale_state(current: Any) -> dict[str, Any]:
    local_status = tailscale_status_summary()
    local_serve_status = tailscale_serve_status_summary()
    remote_target = (
        str(getattr(current, "remote_host", "") or "").strip()
        if str(getattr(current, "access_mode", "local") or "local").strip().lower() == "remote_tailscale"
        else ""
    )
    remote_status = tailscale_status_summary(remote_target) if remote_target else local_status
    remote_serve_status = tailscale_serve_status_summary(remote_target) if remote_target else local_serve_status
    magicdns_name = str(getattr(current, "remote_magicdns_name", "") or remote_status.get("magicdns_name") or "").strip()
    access_mode = str(getattr(current, "access_mode", "local") or "local").strip().lower()
    configured = bool(
        access_mode == "remote_tailscale"
        and remote_status.get("available")
        and remote_status.get("logged_in")
        and magicdns_name
        and remote_serve_status.get("enabled")
    )
    last_check_error = str(remote_status.get("last_check_error") or remote_serve_status.get("last_check_error") or "").strip()
    if access_mode == "remote_tailscale" and not magicdns_name and not last_check_error:
        last_check_error = "Tailscale is signed in, but no MagicDNS name is available on the selected host."
    return {
        "configured": configured,
        "local_available": bool(local_status.get("available")),
        "local_logged_in": bool(local_status.get("logged_in")),
        "remote_available": bool(remote_status.get("available")),
        "remote_logged_in": bool(remote_status.get("logged_in")),
        "remote_magicdns_name": magicdns_name,
        "tailnet_name": str(remote_status.get("tailnet_name") or local_status.get("tailnet_name") or "").strip(),
        "reachable": bool(remote_status.get("reachable") and magicdns_name and remote_serve_status.get("enabled")),
        "serve_enabled": bool(remote_serve_status.get("enabled")),
        "serve_configured": bool(remote_serve_status.get("configured")),
        "serve_enable_url": str(remote_serve_status.get("enable_url") or "").strip(),
        "last_check_error": last_check_error,
    }


def _openclaw_dashboard_state(app: Any) -> dict[str, Any]:
    install_dir = resolve_openclaw_registered_install_path(str(app.install_path or ""))
    current = load_openclaw_instance_config(
        install_dir,
        app.app_config_state if isinstance(app.app_config_state, dict) else {},
    )
    tailscale = _openclaw_tailscale_state(current)
    access_mode = str(current.access_mode or "local").strip().lower()
    local_dashboard_url = _openclaw_dashboard_url(str(install_dir))
    remote_magicdns_name = str(current.remote_magicdns_name or tailscale.get("remote_magicdns_name") or "").strip()
    remote_host = str(current.remote_host or remote_magicdns_name or "").strip()

    if access_mode == "remote_tailscale":
        secure_dashboard_url = ""
        if remote_magicdns_name:
            parsed = urlparse(local_dashboard_url)
            token = parsed.fragment if parsed.scheme else ""
            alias = quote(str(getattr(app, "alias", "") or "openclaw").strip() or "openclaw")
            secure_dashboard_url = f"https://{remote_magicdns_name}/native/openclaw/{alias}/"
            if token:
                secure_dashboard_url = f"{secure_dashboard_url}#{token}"
        dashboard_ready = bool(tailscale.get("configured") and secure_dashboard_url)
        return {
            "dashboard_url": secure_dashboard_url,
            "dashboard_ready": dashboard_ready,
            "dashboard_secure": dashboard_ready,
            "dashboard_requires_secure_context": True,
            "dashboard_block_reason": "" if dashboard_ready else "tailscale_not_configured",
            "access_mode": "remote_tailscale",
            "remote_host": remote_host,
            "remote_magicdns_name": remote_magicdns_name,
            "tailscale": tailscale,
        }

    return {
        "dashboard_url": local_dashboard_url,
        "dashboard_ready": bool(local_dashboard_url),
        "dashboard_secure": True,
        "dashboard_requires_secure_context": False,
        "dashboard_block_reason": "",
        "access_mode": "local",
        "remote_host": "",
        "remote_magicdns_name": "",
        "tailscale": {
            "configured": False,
            "local_available": bool(tailscale.get("local_available")),
            "local_logged_in": bool(tailscale.get("local_logged_in")),
            "remote_available": False,
            "remote_logged_in": False,
            "remote_magicdns_name": "",
            "tailnet_name": str(tailscale.get("tailnet_name") or "").strip(),
            "reachable": False,
            "serve_enabled": bool(tailscale.get("serve_enabled")),
            "serve_configured": bool(tailscale.get("serve_configured")),
            "serve_enable_url": str(tailscale.get("serve_enable_url") or "").strip(),
            "last_check_error": "",
        },
    }


def _read_key_value_file(path: Path) -> dict[str, str]:
    if not path.exists():
        return {}
    try:
        raw = path.read_text(encoding="utf-8")
    except Exception:
        return {}
    values: dict[str, str] = {}
    for line in raw.splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        values[key.strip()] = value.strip()
    return values



def _influencerpy_dashboard_url(install_path: str) -> str:
    root = Path(str(install_path or "").strip())
    values = _read_key_value_file(root / '.influencerpy' / '.env')
    port = str(values.get('INFLUENCERPY_FRONTEND_PORT') or '3080').strip()
    if not port.isdigit():
        return ''
    return f'http://127.0.0.1:{port}/'



def _influencerpy_tailscale_state(app: Any) -> dict[str, Any]:
    state = app.app_config_state if isinstance(app.app_config_state, dict) else {}
    local_status = tailscale_status_summary()
    local_serve_status = tailscale_serve_status_summary()
    access_mode = str(state.get('access_mode') or 'local').strip().lower()
    remote_target = str(state.get('remote_host') or '').strip() if access_mode == 'remote_tailscale' else ''
    remote_status = tailscale_status_summary(remote_target) if remote_target else local_status
    remote_serve_status = tailscale_serve_status_summary(remote_target) if remote_target else local_serve_status
    magicdns_name = str(state.get('remote_magicdns_name') or remote_status.get('magicdns_name') or '').strip()
    configured = bool(
        access_mode == 'remote_tailscale'
        and remote_status.get('available')
        and remote_status.get('logged_in')
        and magicdns_name
        and remote_serve_status.get('enabled')
    )
    last_check_error = str(remote_status.get('last_check_error') or remote_serve_status.get('last_check_error') or '').strip()
    if access_mode == 'remote_tailscale' and not magicdns_name and not last_check_error:
        last_check_error = 'Tailscale is signed in, but no MagicDNS name is available on the selected host.'
    return {
        'configured': configured,
        'local_available': bool(local_status.get('available')),
        'local_logged_in': bool(local_status.get('logged_in')),
        'remote_available': bool(remote_status.get('available')),
        'remote_logged_in': bool(remote_status.get('logged_in')),
        'remote_magicdns_name': magicdns_name,
        'tailnet_name': str(remote_status.get('tailnet_name') or local_status.get('tailnet_name') or '').strip(),
        'reachable': bool(remote_status.get('reachable') and magicdns_name and remote_serve_status.get('enabled')),
        'serve_enabled': bool(remote_serve_status.get('enabled')),
        'serve_configured': bool(remote_serve_status.get('configured')),
        'serve_enable_url': str(remote_serve_status.get('enable_url') or '').strip(),
        'last_check_error': last_check_error,
    }



def _influencerpy_dashboard_state(app: Any) -> dict[str, Any]:
    install_dir = Path(str(app.install_path or '')).expanduser().resolve()
    state = app.app_config_state if isinstance(app.app_config_state, dict) else {}
    tailscale = _influencerpy_tailscale_state(app)
    access_mode = str(state.get('access_mode') or 'local').strip().lower()
    local_dashboard_url = _influencerpy_dashboard_url(str(install_dir))
    remote_magicdns_name = str(state.get('remote_magicdns_name') or tailscale.get('remote_magicdns_name') or '').strip()
    remote_host = str(state.get('remote_host') or remote_magicdns_name or '').strip()

    if access_mode == 'remote_tailscale':
        secure_dashboard_url = ''
        if remote_magicdns_name:
            alias = quote(str(getattr(app, 'alias', '') or getattr(app, 'name', '') or 'influencerpy').strip() or 'influencerpy')
            secure_dashboard_url = f'https://{remote_magicdns_name}/native/influencerpy/{alias}/'
        dashboard_ready = bool(tailscale.get('configured') and secure_dashboard_url)
        return {
            'dashboard_url': secure_dashboard_url,
            'dashboard_ready': dashboard_ready,
            'dashboard_secure': dashboard_ready,
            'dashboard_requires_secure_context': True,
            'dashboard_block_reason': '' if dashboard_ready else 'tailscale_not_configured',
            'access_mode': 'remote_tailscale',
            'remote_host': remote_host,
            'remote_magicdns_name': remote_magicdns_name,
            'tailscale': tailscale,
        }

    return {
        'dashboard_url': local_dashboard_url,
        'dashboard_ready': bool(local_dashboard_url),
        'dashboard_secure': False,
        'dashboard_requires_secure_context': False,
        'dashboard_block_reason': '',
        'access_mode': 'local',
        'remote_host': '',
        'remote_magicdns_name': '',
        'tailscale': {
            'configured': False,
            'local_available': bool(tailscale.get('local_available')),
            'local_logged_in': bool(tailscale.get('local_logged_in')),
            'remote_available': False,
            'remote_logged_in': False,
            'remote_magicdns_name': '',
            'tailnet_name': str(tailscale.get('tailnet_name') or '').strip(),
            'reachable': False,
            'serve_enabled': bool(tailscale.get('serve_enabled')),
            'serve_configured': bool(tailscale.get('serve_configured')),
            'serve_enable_url': str(tailscale.get('serve_enable_url') or '').strip(),
            'last_check_error': '',
        },
    }



def _dashboard_state_for_app(app: Any) -> dict[str, Any]:
    agent_key = str(app.agent_id or app.name or '').strip().lower()
    if agent_key == 'openclaw':
        return _openclaw_dashboard_state(app)
    if agent_key == 'influencerpy':
        return _influencerpy_dashboard_state(app)
    return {
        'dashboard_url': '',
        'dashboard_ready': False,
        'dashboard_secure': False,
        'dashboard_requires_secure_context': False,
        'dashboard_block_reason': '',
        'access_mode': 'local',
        'remote_host': '',
        'remote_magicdns_name': '',
        'tailscale': None,
    }



def influencerpy_dashboard_upstream(agent: str) -> dict[str, Any]:
    alias = str(agent or '').strip()
    if not alias:
        return {'ok': False, 'message': 'InfluencerPy agent alias is required.'}
    reg = AppRegistry()
    app = reg.get_app(alias)
    if not app:
        return {'ok': False, 'message': f"InfluencerPy agent '{alias}' is not installed."}
    agent_id = str(app.agent_id or app.name or '').strip().lower()
    if agent_id != 'influencerpy':
        return {'ok': False, 'message': f"Agent '{alias}' does not expose an InfluencerPy dashboard."}
    install_dir = Path(str(app.install_path or '')).expanduser().resolve()
    values = _read_key_value_file(install_dir / '.influencerpy' / '.env')
    frontend_port = str(values.get('INFLUENCERPY_FRONTEND_PORT') or '3080').strip()
    backend_port = str(values.get('INFLUENCERPY_BACKEND_PORT') or '8789').strip()
    if not frontend_port.isdigit():
        return {'ok': False, 'message': f"InfluencerPy dashboard port is unavailable for '{alias}'."}
    if not backend_port.isdigit():
        return {'ok': False, 'message': f"InfluencerPy API port is unavailable for '{alias}'."}
    return {
        'ok': True,
        'agent': alias,
        'http_base': f'http://127.0.0.1:{frontend_port}',
        'ws_base': f'ws://127.0.0.1:{frontend_port}',
        'api_base': f'http://127.0.0.1:{backend_port}/api',
        'mount_path': f'/native/influencerpy/{alias}',
    }


def _armorer_base_urls() -> dict[str, str]:
    local_url = "http://127.0.0.1:3000"
    local_status = tailscale_status_summary()
    local_serve_status = tailscale_serve_status_summary()
    magicdns_name = str(local_status.get("magicdns_name") or "").strip()
    secure_url = ""
    if magicdns_name and local_serve_status.get("enabled"):
        secure_url = f"https://{magicdns_name}"
    return {
        "local_url": local_url,
        "secure_url": secure_url,
    }


def urls_summary(agent: str = "") -> dict[str, Any]:
    requested = str(agent or "").strip().lower()
    installed = installed_agents_summary()
    if not installed.get("ok"):
        return {"ok": False, "message": "Armorer could not read installed agents."}

    base_urls = _armorer_base_urls()
    items: list[dict[str, Any]] = []
    notes: list[str] = []

    if not requested:
        items.append(
            {
                "label": "Armorer",
                "kind": "armorer",
                "url": str(base_urls.get("secure_url") or base_urls.get("local_url") or "").strip(),
                "access": "private_tailscale" if base_urls.get("secure_url") else "local",
                "ready": True,
                "note": "Private via Tailscale" if base_urls.get("secure_url") else "Local web UI",
            }
        )
        if base_urls.get("secure_url"):
            items.append(
                {
                    "label": "Armorer (local fallback)",
                    "kind": "armorer_local",
                    "url": str(base_urls.get("local_url") or "").strip(),
                    "access": "local",
                    "ready": True,
                    "note": "Works on the host machine",
                }
            )

    matched_any = False
    for entry in installed.get("agents", []) or []:
        if not isinstance(entry, dict):
            continue
        alias = str(entry.get("alias") or "").strip()
        if requested and requested not in {alias.lower(), str(entry.get("agent_id") or "").strip().lower()}:
            continue
        matched_any = True
        dashboard_url = str(entry.get("dashboard_url") or "").strip()
        dashboard_ready = bool(entry.get("dashboard_ready"))
        if dashboard_url:
            items.append(
                {
                    "label": f"{alias} dashboard",
                    "kind": "dashboard",
                    "url": dashboard_url,
                    "access": str(entry.get("access_mode") or "local").strip() or "local",
                    "ready": dashboard_ready,
                    "note": "Ready" if dashboard_ready else str(entry.get("dashboard_block_reason") or "").strip(),
                }
            )
        setup = entry.get("setup")
        if isinstance(setup, dict) and setup.get("requires_setup"):
            items.append(
                {
                    "label": f"{alias} setup",
                    "kind": "setup",
                    "url": f"{str(base_urls.get('secure_url') or base_urls.get('local_url') or '').rstrip('/')}/agents/installed/{quote(alias)}?setup=1",
                    "access": str(entry.get("access_mode") or "local").strip() or "local",
                    "ready": True,
                    "note": "Setup required",
                }
            )
        tailscale = entry.get("tailscale")
        if isinstance(tailscale, dict):
            enable_url = str(tailscale.get("serve_enable_url") or "").strip()
            if enable_url:
                notes.append(f"{alias}: enable Tailscale Serve at {enable_url}")

    if requested and not matched_any:
        return {"ok": False, "message": f"No installed agent matched '{requested}'."}

    return {
        "ok": True,
        "requested_agent": requested,
        "items": items,
        "notes": notes,
    }


def _sync_openclaw_control_ui_origin(install_dir: Path, ui_origin: str) -> bool:
    origin = str(ui_origin or "").strip()
    if not origin:
        return False
    parsed = urlparse(origin)
    if parsed.scheme not in {"http", "https"} or not parsed.netloc:
        return False

    config_path = install_dir / ".openclaw" / "openclaw.json"
    if not config_path.exists():
        return False
    try:
        data = json.loads(config_path.read_text(encoding="utf-8") or "{}")
    except Exception:
        return False
    if not isinstance(data, dict):
        return False

    gateway = data.setdefault("gateway", {})
    if not isinstance(gateway, dict):
        gateway = {}
        data["gateway"] = gateway
    control_ui = gateway.setdefault("controlUi", {})
    if not isinstance(control_ui, dict):
        control_ui = {}
        gateway["controlUi"] = control_ui

    existing = control_ui.get("allowedOrigins")
    merged: list[str] = []
    if isinstance(existing, list):
        for item in existing:
            if isinstance(item, str) and item not in merged:
                merged.append(item)
    if origin not in merged:
        merged.append(origin)
    control_ui["allowedOrigins"] = merged

    serialized = json.dumps(data, indent=2) + "\n"
    try:
        current = config_path.read_text(encoding="utf-8")
    except Exception:
        current = ""
    if current == serialized:
        return False
    config_path.write_text(serialized, encoding="utf-8")
    return True


def openclaw_native_ui_upstream(agent: str) -> dict[str, Any]:
    alias = str(agent or "").strip()
    if not alias:
        return {"ok": False, "message": "OpenClaw agent alias is required."}
    reg = AppRegistry()
    app = reg.get_app(alias)
    if not app:
        return {"ok": False, "message": f"OpenClaw agent '{alias}' is not installed."}
    agent_id = str(app.agent_id or app.name or "").strip().lower()
    if agent_id != "openclaw":
        return {"ok": False, "message": f"Agent '{alias}' does not expose an OpenClaw native UI."}
    install_dir = resolve_openclaw_registered_install_path(str(app.install_path or ""))
    dashboard_url = _openclaw_dashboard_url(str(install_dir))
    if not dashboard_url:
        return {"ok": False, "message": f"OpenClaw native UI is unavailable for '{alias}'."}
    parsed = urlparse(dashboard_url)
    port = parsed.port
    if not port:
        return {"ok": False, "message": f"OpenClaw native UI port is unavailable for '{alias}'."}
    return {
        "ok": True,
        "agent": alias,
        "http_base": f"http://127.0.0.1:{port}",
        "ws_base": f"ws://127.0.0.1:{port}",
    }


def installed_agents_summary() -> dict[str, Any]:
    reg = AppRegistry()
    apps = reg.list_apps()
    status_map: dict[str, dict[str, Any]] = {}
    try:
        structured = list_agents_structured()
        for item in structured.get("agents", []) or []:
            if not isinstance(item, dict):
                continue
            alias = str(item.get("alias") or "").strip()
            if alias:
                status_map[alias] = dict(item.get("status") or {})
    except Exception:
        status_map = {}
    return {
        "ok": True,
        "count": len(apps),
        "agents": [
            {
                "alias": a.name,
                "agent_id": a.agent_id or a.name,
                "install_path": a.install_path,
                "description": a.description,
                "run_commands": list(a.run_commands or []),
                "channels_enabled": list(a.channels_enabled or []),
                "skills_enabled": list(a.skills_enabled or []),
                "status": status_map.get(a.name, {}),
                **_dashboard_state_for_app(a),
                "setup": _openclaw_setup_snapshot(a.name) if str(a.agent_id or a.name).strip().lower() == "openclaw" else {},
            }
            for a in apps
        ],
    }


def approve_openclaw_native_ui_browser(agent: str, *, opened_at_ms: int = 0, ui_origin: str = "") -> dict[str, Any]:
    alias = str(agent or "").strip()
    if not alias:
        return {"ok": False, "message": "OpenClaw agent alias is required."}
    reg = AppRegistry()
    app = reg.get_app(alias)
    if not app:
        return {"ok": False, "message": f"OpenClaw agent '{alias}' is not installed."}
    agent_id = str(app.agent_id or app.name or "").strip().lower()
    if agent_id != "openclaw":
        return {"ok": False, "message": f"Agent '{alias}' does not expose an OpenClaw native UI."}
    install_dir = resolve_openclaw_registered_install_path(str(app.install_path or ""))
    _sync_openclaw_control_ui_origin(install_dir, ui_origin)
    result = approve_openclaw_browser_device_pairing(install_dir, opened_at_ms=int(opened_at_ms or 0))
    result["agent"] = alias
    result["dashboard_url"] = _openclaw_dashboard_url(str(install_dir))
    return result


def _armorer_telegram_identity() -> dict[str, Any]:
    cm = ConfigManager()
    guard_settings = _load_guard_settings(Path(cm.CONFIG_DIR))
    alert_chat_id = str(guard_settings.get("telegram_alert_chat_id") or cm.get_key("TELEGRAM_ALERT_CHAT_ID") or "").strip()
    admin_chat_ids = list(guard_settings.get("telegram_admin_chat_ids", []) or _split_list(cm.get_key("ARMORER_TELEGRAM_ADMIN_CHAT_IDS")))
    admin_user_ids = list(guard_settings.get("telegram_admin_user_ids", []) or _split_list(cm.get_key("ARMORER_TELEGRAM_ADMIN_USER_IDS")))
    available = bool(alert_chat_id or admin_chat_ids or admin_user_ids)
    return {
        "available": available,
        "alert_chat_id": alert_chat_id,
        "admin_chat_ids": admin_chat_ids,
        "admin_user_ids": admin_user_ids,
    }


def _openclaw_setup_state_from_context(context: dict[str, Any]) -> dict[str, Any]:
    current = dict(context.get("current_config") or {})
    state_summary = dict(context.get("state_summary") or {})
    channel_state = dict(state_summary.get("channel_state") or {})
    tailscale = dict(context.get("tailscale") or {})
    enabled_channels = list(current.get("enabled_channels") or [])
    enabled_skills = list(current.get("enabled_skills") or [])
    provider = str(current.get("primary_provider") or state_summary.get("provider") or "").strip()
    model = str(current.get("default_model") or state_summary.get("model") or "").strip()
    provider_credential_present = bool(state_summary.get("provider_credential_present"))
    telegram_enabled = "telegram" in enabled_channels
    telegram_channel = dict(channel_state.get("telegram") or {})
    telegram_ready = (not telegram_enabled) or not list(telegram_channel.get("missing_keys") or [])
    requires_setup = bool((not enabled_channels) or (not provider_credential_present) or (telegram_enabled and not telegram_ready))
    return {
        "requires_setup": requires_setup,
        "provider": provider,
        "model": model,
        "provider_credential_present": provider_credential_present,
        "enabled_channels": enabled_channels,
        "enabled_skills": enabled_skills,
        "telegram_ready": telegram_ready,
        "access_mode": str(current.get("access_mode") or "local").strip() or "local",
        "remote_host": str(current.get("remote_host") or "").strip(),
        "remote_magicdns_name": str(current.get("remote_magicdns_name") or tailscale.get("remote_magicdns_name") or "").strip(),
        "dashboard_ready": bool(
            str(current.get("access_mode") or "local").strip() != "remote_tailscale"
            or tailscale.get("configured")
        ),
    }


def _openclaw_setup_snapshot(agent: str) -> dict[str, Any]:
    registry = AppRegistry()
    app = registry.get_app(agent)
    if not app or str(app.agent_id or app.name).strip().lower() != "openclaw":
        return {}
    try:
        install_dir = resolve_openclaw_registered_install_path(app.install_path)
        cfg = ConfigManager(env_path=str(install_dir / ".env"))
        current = load_openclaw_instance_config(
            install_dir,
            app.app_config_state if isinstance(app.app_config_state, dict) else {},
        )
    except Exception:
        return {}

    provider_env_map = {
        "openai": "OPENAI_API_KEY",
        "anthropic": "ANTHROPIC_API_KEY",
        "gemini": "GEMINI_API_KEY",
        "openrouter": "OPENROUTER_API_KEY",
        "ollama": "OLLAMA_BASE_URL",
    }
    channel_env_keys = {
        "telegram": ["TELEGRAM_BOT_TOKEN"],
        "discord": ["DISCORD_BOT_TOKEN"],
        "slack": ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"],
    }
    provider_env = provider_env_map.get(str(current.primary_provider or "").strip().lower(), "")
    provider_credential_present = bool(provider_env and (cfg.find_vault_key(provider_env) or cfg.get_key(provider_env)))
    channel_state = {
        channel: {
            "missing_keys": [
                key_name
                for key_name in channel_env_keys.get(channel, [])
                if not (cfg.find_vault_key(key_name) or cfg.get_key(key_name))
            ],
        }
        for channel in channel_env_keys
    }
    context = {
        "current_config": {
            "primary_provider": current.primary_provider,
            "default_model": current.default_model,
            "enabled_channels": list(current.enabled_channels or []),
            "enabled_skills": list(current.enabled_skills or []),
        },
        "state_summary": {
            "provider": current.primary_provider,
            "model": current.default_model,
            "provider_credential_present": provider_credential_present,
            "channel_state": channel_state,
        },
    }
    return _openclaw_setup_state_from_context(context)


def openclaw_setup_summary(agent: str) -> dict[str, Any]:
    context = get_openclaw_setup_context(agent)
    if not context.get("ok"):
        return context
    registry = AppRegistry()
    app = registry.get_app(agent)
    pending_pairings: list[dict[str, Any]] = []
    setup = _openclaw_setup_state_from_context(context)
    if app:
        install_dir = resolve_openclaw_registered_install_path(app.install_path)
        cfg = ConfigManager()
        telegram_bot_identity = _telegram_bot_identity(
            str(cfg.get_scoped_secret(agent, "TELEGRAM_BOT_TOKEN", fallback_to_global=False) or "").strip()
        )
        if telegram_bot_identity:
            setup["telegram_bot_handle"] = telegram_bot_identity.get("handle", "")
            setup["telegram_bot_username"] = telegram_bot_identity.get("username", "")
            setup["telegram_bot_name"] = telegram_bot_identity.get("name", "")
        try:
            pending_pairings = discover_openclaw_pairing_requests(install_dir, "telegram")
        except Exception:
            pending_pairings = []
        try:
            if "telegram" in list(setup.get("enabled_channels") or []):
                telegram_status = (verify_openclaw_channels(install_dir, ["telegram"]).get("statuses") or {}).get("telegram") or {}
                setup["telegram_configured"] = bool(telegram_status.get("configured"))
                setup["telegram_verified"] = bool(telegram_status.get("verified"))
                setup["telegram_linked"] = bool(telegram_status.get("linked"))
                setup["telegram_detail"] = str(telegram_status.get("detail") or "").strip()
        except Exception:
            pass
        try:
            current = load_openclaw_instance_config(
                install_dir,
                app.app_config_state if isinstance(app.app_config_state, dict) else {},
            )
            setup["tailscale"] = _openclaw_tailscale_state(current)
        except Exception:
            pass
    return {
        "ok": True,
        "agent": agent,
        "setup": setup,
        "context": context,
        "armorer_telegram_identity": _armorer_telegram_identity(),
        "pending_pairings": pending_pairings,
    }


def apply_openclaw_setup(agent: str, payload: dict[str, Any]) -> dict[str, Any]:
    access_mode = str(payload.get("accessMode") or "local").strip().lower()
    if access_mode != "remote_tailscale":
        access_mode = "local"
    remote_host = str(payload.get("remoteHost") or "").strip()
    provider = str(payload.get("provider") or "").strip().lower()
    model = str(payload.get("model") or "").strip()
    telegram_enabled = bool(payload.get("telegramEnabled", True))
    reuse_identity = bool(payload.get("reuseArmorerTelegramIdentity"))
    enabled_skills = [str(item or "").strip() for item in (payload.get("enabledSkills") or []) if str(item or "").strip()]
    web_search_provider = str(payload.get("webSearchProvider") or "disabled").strip() or "disabled"
    pairing_code = str(payload.get("pairingCode") or "").strip()

    provider_key_map = dict(OPENCLAW_PROVIDER_ENV_KEYS)
    web_key_map = {
        "brave": "BRAVE_API_KEY",
        "perplexity": "PERPLEXITY_API_KEY",
        "gemini": "GEMINI_API_KEY",
        "grok": "XAI_API_KEY",
        "kimi": "MOONSHOT_API_KEY",
    }

    config_patch: dict[str, Any] = {
        "access_mode": access_mode,
        "remote_host": remote_host,
        "primary_provider": provider,
        "default_model": model,
        "enabled_channels": ["telegram"] if telegram_enabled else [],
        "enabled_skills": enabled_skills,
        "web_search_provider": web_search_provider,
    }
    secrets: dict[str, str] = {}

    provider_key = provider_key_map.get(provider, "")
    provider_secret = str(payload.get("providerApiKey") or "").strip()
    if provider_key and provider_secret:
        secrets[provider_key] = provider_secret

    telegram_bot_token = str(payload.get("telegramBotToken") or "").strip()
    if telegram_enabled and telegram_bot_token:
        secrets["TELEGRAM_BOT_TOKEN"] = telegram_bot_token

    web_key = web_key_map.get(web_search_provider, "")
    web_secret = str(payload.get("webSearchApiKey") or "").strip()
    if web_key and web_secret:
        secrets[web_key] = web_secret

    cfg = ConfigManager()
    armorer_telegram_token = str(cfg.get_armorer_telegram_bot_token() or cfg.get_key("TELEGRAM_BOT_TOKEN") or "").strip()
    effective_openclaw_telegram_token = telegram_bot_token or str(
        cfg.get_scoped_secret(agent, "TELEGRAM_BOT_TOKEN", fallback_to_global=False) or ""
    ).strip()
    if telegram_enabled and effective_openclaw_telegram_token and armorer_telegram_token and effective_openclaw_telegram_token == armorer_telegram_token:
        return {
            "ok": False,
            "message": "OpenClaw cannot use the same Telegram bot token as Armorer. Add a dedicated bot token for this agent.",
            "errors": [
                "The OpenClaw Telegram bot token matches Armorer's Telegram bot token. Use a different bot token so the two runtimes do not fight over the same Telegram updates.",
            ],
        }

    matched_pairing_request: dict[str, Any] | None = None
    pairing_codes = {"telegram": pairing_code} if pairing_code and telegram_enabled else None
    if telegram_enabled and not pairing_codes:
        registry = AppRegistry()
        app = registry.get_app(agent)
        if app:
            install_dir = resolve_openclaw_registered_install_path(app.install_path)
            try:
                pending_pairings = discover_openclaw_pairing_requests(install_dir, "telegram")
            except Exception:
                pending_pairings = []
            if len(pending_pairings) == 1:
                detected_code = str(pending_pairings[0].get("code") or "").strip()
                if detected_code:
                    matched_pairing_request = pending_pairings[0]
                    pairing_codes = {"telegram": detected_code}
    elif pairing_codes and telegram_enabled:
        registry = AppRegistry()
        app = registry.get_app(agent)
        if app:
            install_dir = resolve_openclaw_registered_install_path(app.install_path)
            try:
                pending_pairings = discover_openclaw_pairing_requests(install_dir, "telegram")
            except Exception:
                pending_pairings = []
            wanted = str((pairing_codes or {}).get("telegram") or "").strip().upper()
            for item in pending_pairings:
                if str(item.get("code") or "").strip().upper() == wanted:
                    matched_pairing_request = item
                    break
    result = apply_openclaw_config(
        agent,
        config_patch=config_patch,
        secrets=secrets,
        bind_channels=True,
        pairing_codes=pairing_codes,
    )
    if result.get("ok") and telegram_enabled and reuse_identity:
        identity = _armorer_telegram_identity()
        preferred_account = (
            (identity.get("admin_user_ids") or [None])[0]
            or (identity.get("admin_chat_ids") or [None])[0]
            or identity.get("alert_chat_id")
            or ""
        )
        extra_ids = list(dict.fromkeys([
            *list(identity.get("admin_user_ids") or []),
            *list(identity.get("admin_chat_ids") or []),
            str(identity.get("alert_chat_id") or "").strip(),
        ]))
        extra_ids = [item for item in extra_ids if item]
        if preferred_account:
            registry = AppRegistry()
            app = registry.get_app(agent)
            if app:
                install_dir = resolve_openclaw_registered_install_path(app.install_path)
                allowlist_result = seed_openclaw_telegram_allowlist(
                    install_dir,
                    account_id=str(preferred_account),
                    extra_ids=extra_ids,
                )
                result["allowlist_seed"] = allowlist_result
                result["warnings"] = list(result.get("warnings") or []) + list(allowlist_result.get("warnings") or [])
    pairing_status = ((result.get("pairing_approval") or {}).get("statuses") or {}).get("telegram") or {}
    if result.get("ok") and pairing_status.get("approved") and matched_pairing_request:
        detected_account_id = str(matched_pairing_request.get("account_id") or "").strip()
        if detected_account_id:
            registry = AppRegistry()
            app = registry.get_app(agent)
            if app:
                install_dir = resolve_openclaw_registered_install_path(app.install_path)
                allowlist_result = seed_openclaw_telegram_allowlist(
                    install_dir,
                    account_id=detected_account_id,
                    extra_ids=[],
                )
                result["pairing_allowlist_seed"] = allowlist_result
                result["warnings"] = list(result.get("warnings") or []) + list(allowlist_result.get("warnings") or [])
    result["setup"] = openclaw_setup_summary(agent).get("setup", {})
    return result


def _openclaw_install_dir() -> Path:
    install_dir = Path.cwd() / "installed" / "openclaw"
    if not install_dir.exists():
        raise FileNotFoundError("OpenClaw install directory not found.")
    return install_dir


def _openclaw_sessions_dir() -> Path:
    return _openclaw_install_dir() / ".openclaw" / "agents" / "main" / "sessions"


def _strip_final_tags(text: str) -> str:
    return re.sub(r"</?final>", "", text, flags=re.IGNORECASE).strip()


def _extract_openclaw_message_text(entry: dict[str, Any]) -> str:
    message = entry.get("message")
    if not isinstance(message, dict):
        return ""
    content = message.get("content")
    if not isinstance(content, list):
        return ""
    parts: list[str] = []
    for item in content:
        if not isinstance(item, dict):
            continue
        if str(item.get("type") or "").strip().lower() != "text":
            continue
        text = str(item.get("text") or "").strip()
        if text:
            parts.append(text)
    return _strip_final_tags("\n".join(parts).strip())


def openclaw_chat_history(session_id: str, limit: int = 60) -> dict[str, Any]:
    normalized_session = str(session_id or "").strip()
    if not normalized_session:
        raise ValueError("session_id is required")
    transcript_path = _openclaw_sessions_dir() / f"{normalized_session}.jsonl"
    if not transcript_path.exists():
        return {"ok": True, "session_id": normalized_session, "messages": []}

    messages: list[dict[str, Any]] = []
    for raw_line in transcript_path.read_text(encoding="utf-8").splitlines():
        line = raw_line.strip()
        if not line:
            continue
        try:
            entry = json.loads(line)
        except Exception:
            continue
        if not isinstance(entry, dict):
            continue
        if str(entry.get("type") or "").strip().lower() != "message":
            continue
        text = _extract_openclaw_message_text(entry)
        if not text:
            continue
        message = entry.get("message")
        role = str(message.get("role") or "").strip().lower() if isinstance(message, dict) else ""
        messages.append(
            {
                "role": role if role in {"user", "assistant", "system"} else "assistant",
                "text": text,
                "timestamp": entry.get("timestamp"),
            }
        )
    trimmed = messages[-max(1, min(int(limit), 500)) :]
    return {"ok": True, "session_id": normalized_session, "messages": trimmed}


def run_openclaw_chat_turn(prompt: str, session_id: str) -> dict[str, Any]:
    normalized_prompt = str(prompt or "").strip()
    normalized_session = str(session_id or "").strip()
    if not normalized_prompt:
        raise ValueError("prompt is required")
    if not normalized_session:
        raise ValueError("session_id is required")
    config_dir = cli_common.armorer_config_dir()
    guard = ConversationGuard(config_dir=config_dir)
    conversation_id = f"openclaw:web:{normalized_session}"
    in_scan = guard.inspect_input(normalized_prompt)
    guard.append_history(
        conversation_id=conversation_id,
        role="user",
        text=normalized_prompt,
        inspection=in_scan,
    )
    armorer_db.insert_runtime_event(
        config_dir,
        {
            "ts": time.time(),
            "agent_alias": "openclaw",
            "agent_id": "openclaw",
            "event_type": "chat",
            "subtype": "user_prompt",
            "severity": "warn" if in_scan.suspicious else "info",
            "verdict": "suspicious" if in_scan.suspicious else "observed",
            "raw": {
                "session_id": normalized_session,
                "text": in_scan.sanitized_text[:400],
                "reasons": in_scan.reasons,
                "confidence": in_scan.confidence,
            },
        },
    )
    if in_scan.suspicious:
        guard.notify_suspicious(
            channel_id="",
            conversation_id=conversation_id,
            source="openclaw_web_chat",
            snippet=normalized_prompt,
            reasons=in_scan.reasons,
            confidence=in_scan.confidence,
            app_name="openclaw",
            input_preview=in_scan.sanitized_text,
        )

    completed = subprocess.run(
        [
            "docker",
            "compose",
            "-f",
            "docker-compose.yml",
            "exec",
            "-T",
            "openclaw-gateway",
            "node",
            "dist/index.js",
            "agent",
            "--message",
            normalized_prompt,
            "--session-id",
            normalized_session,
            "--json",
        ],
        cwd=str(_openclaw_install_dir()),
        capture_output=True,
        text=True,
        timeout=180,
        check=False,
    )
    if completed.returncode != 0:
        detail = (completed.stderr or completed.stdout or "OpenClaw chat command failed.").strip()
        raise RuntimeError(detail)

    stdout = str(completed.stdout or "").strip()
    json_start = stdout.find("{")
    if json_start < 0:
        raise RuntimeError("OpenClaw chat command returned non-JSON output.")
    payload = json.loads(stdout[json_start:])
    result = payload.get("result") if isinstance(payload, dict) else {}
    payloads = result.get("payloads") if isinstance(result, dict) else []
    reply_text = ""
    if isinstance(payloads, list):
        for item in payloads:
            if not isinstance(item, dict):
                continue
            text = str(item.get("text") or "").strip()
            if text:
                reply_text = text
                break
    out_scan = guard.inspect_output(reply_text)
    guard.append_history(
        conversation_id=conversation_id,
        role="assistant",
        text=reply_text,
        inspection=out_scan,
    )
    armorer_db.insert_runtime_event(
        config_dir,
        {
            "ts": time.time(),
            "agent_alias": "openclaw",
            "agent_id": "openclaw",
            "event_type": "chat",
            "subtype": "assistant_reply",
            "severity": "warn" if out_scan.suspicious else "info",
            "verdict": "suspicious" if out_scan.suspicious else "observed",
            "raw": {
                "session_id": normalized_session,
                "text": out_scan.sanitized_text[:400],
                "reasons": out_scan.reasons,
                "confidence": out_scan.confidence,
            },
        },
    )
    if out_scan.suspicious:
        guard.notify_suspicious(
            channel_id="",
            conversation_id=conversation_id,
            source="openclaw_web_chat",
            snippet=reply_text,
            reasons=out_scan.reasons,
            confidence=out_scan.confidence,
            app_name="openclaw",
            output_preview=out_scan.sanitized_text,
        )

    history = openclaw_chat_history(normalized_session, limit=60)
    return {
        "ok": True,
        "session_id": normalized_session,
        "message": out_scan.sanitized_text,
        "raw": payload,
        "history": history.get("messages", []),
    }
