import json
import os
import secrets
import stat
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any, Dict, List
from urllib.parse import urlsplit, urlunsplit

from armorer.config import ConfigManager
from armorer.openclaw_config import (
    canonicalize_openclaw_mount_env_paths,
    derive_openclaw_control_ui_origins,
    ensure_openclaw_bootstrap_config,
    ensure_openclaw_instance_ports,
    resolve_openclaw_runtime_image,
)


OPENCLAW_PROVIDER_ENV_KEYS = {
    "openai": "OPENAI_API_KEY",
    "anthropic": "ANTHROPIC_API_KEY",
    "gemini": "GEMINI_API_KEY",
    "google": "GEMINI_API_KEY",
    "groq": "GROQ_API_KEY",
    "xai": "XAI_API_KEY",
    "moonshot": "MOONSHOT_API_KEY",
    "kimi": "KIMI_API_KEY",
    "mistral": "MISTRAL_API_KEY",
    "together": "TOGETHER_API_KEY",
    "cerebras": "CEREBRAS_API_KEY",
    "perplexity": "PERPLEXITY_API_KEY",
    "huggingface": "HUGGINGFACE_HUB_TOKEN",
    "nvidia": "NVIDIA_API_KEY",
    "qianfan": "QIANFAN_API_KEY",
    "volcengine": "VOLCANO_ENGINE_API_KEY",
    "byteplus": "BYTEPLUS_API_KEY",
    "venice": "VENICE_API_KEY",
    "sglang": "SGLANG_API_KEY",
    "zai": "ZAI_API_KEY",
    "modelstudio": "MODELSTUDIO_API_KEY",
    "synthetic": "SYNTHETIC_API_KEY",
    "litellm": "LITELLM_API_KEY",
    "openrouter": "OPENROUTER_API_KEY",
    "ollama": "OLLAMA_BASE_URL",
}

_PROVIDER_WRITE_ALIASES = {
    "gemini": "google",
}

_PROVIDER_READ_ALIASES = {
    "google": "gemini",
}

_CHANNEL_ENV_KEYS = {
    "telegram": ["TELEGRAM_BOT_TOKEN"],
    "discord": ["DISCORD_BOT_TOKEN"],
    "slack": ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"],
}

_WEB_ENV_KEYS = {
    "brave": "BRAVE_API_KEY",
    "perplexity": "PERPLEXITY_API_KEY",
    "gemini": "GEMINI_API_KEY",
    "grok": "XAI_API_KEY",
    "kimi": "MOONSHOT_API_KEY",
}

_OPENCLAW_RUNTIME_UID = 1000
_OPENCLAW_RUNTIME_GID = 1000
_OTEL_HOST_ALIAS = "host.docker.internal"
_OTEL_DEFAULT_PORT = "4318"


def _qualified_model(provider: str, model: str) -> str:
    provider = str(provider or "").strip()
    model = str(model or "").strip()
    if not provider:
        return model
    provider = _PROVIDER_WRITE_ALIASES.get(provider, provider)
    if "/" in model:
        return model
    return f"{provider}/{model}"


def _split_qualified_model(value: str) -> tuple[str, str]:
    text = str(value or "").strip()
    if "/" not in text:
        return "", text
    provider, model = text.split("/", 1)
    normalized_provider = _PROVIDER_READ_ALIASES.get(provider.strip(), provider.strip())
    return normalized_provider, model.strip()


def _normalize_ollama_runtime_url(value: str) -> str:
    url = str(value or "").strip() or "http://localhost:11434/v1/"
    while url.endswith("/"):
        url = url[:-1]
    if url.endswith("/v1"):
        url = url[:-3]
    url = url.replace("://localhost", "://host.docker.internal")
    url = url.replace("://127.0.0.1", "://host.docker.internal")
    return url


def _valid_gateway_token(value: str) -> bool:
    token = str(value or "").strip()
    return len(token) >= 16


@dataclass
class OpenClawInstanceConfig:
    access_mode: str = "local"
    remote_host: str = ""
    remote_magicdns_name: str = ""
    primary_provider: str = "openai"
    default_model: str = "gpt-4o"
    enabled_channels: List[str] = field(default_factory=list)
    enabled_skills: List[str] = field(default_factory=list)
    trusted_proxies: List[str] = field(default_factory=list)
    skill_settings: Dict[str, Dict[str, Any]] = field(default_factory=dict)
    web_search_provider: str = "disabled"


def _load_env_map(env_path: Path) -> Dict[str, str]:
    env: Dict[str, str] = {}
    try:
        exists = env_path.exists()
    except Exception:
        return env
    if not exists:
        return env
    try:
        lines = env_path.read_text().splitlines()
    except Exception:
        return env
    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        env[key.strip()] = value.strip()
    return env


def _save_env_map(env_path: Path, env: Dict[str, str]) -> None:
    existing_lines = env_path.read_text().splitlines() if env_path.exists() else []
    output: List[str] = []
    seen = set()
    for line in existing_lines:
        stripped = line.strip()
        if not stripped or stripped.startswith("#") or "=" not in line:
            output.append(line)
            continue
        key, _ = line.split("=", 1)
        key = key.strip()
        if key in env:
            output.append(f"{key}={env[key]}")
            seen.add(key)
        else:
            output.append(line)
    for key, value in env.items():
        if key in seen:
            continue
        if output and output[-1].strip():
            output.append("")
        output.append(f"{key}={value}")
        seen.add(key)
    env_path.write_text("\n".join(output).rstrip() + "\n")


def _sync_env_map(env_path: Path, updates: Dict[str, str], managed_keys: List[str]) -> None:
    env_path.parent.mkdir(parents=True, exist_ok=True)
    env = _load_env_map(env_path)
    managed = {str(key).strip() for key in managed_keys if str(key).strip()}
    for key in managed:
        env.pop(key, None)
    for key, value in updates.items():
        key_name = str(key).strip()
        if not key_name:
            continue
        normalized = str(value or "").strip()
        if normalized:
            env[key_name] = normalized
    existing_order = [line.split("=", 1)[0].strip() for line in env_path.read_text().splitlines() if "=" in line] if env_path.exists() else []
    ordered_keys: List[str] = []
    seen = set()
    for key in existing_order:
        if key in env and key not in seen:
            ordered_keys.append(key)
            seen.add(key)
    for key in env:
        if key not in seen:
            ordered_keys.append(key)
            seen.add(key)
    lines = [f"{key}={env[key]}" for key in ordered_keys]
    env_path.write_text(("\n".join(lines) + "\n") if lines else "")


def _load_json_dict(path: Path) -> Dict[str, Any]:
    try:
        exists = path.exists()
    except Exception:
        return {}
    if not exists:
        return {}
    try:
        data = json.loads(path.read_text())
        return data if isinstance(data, dict) else {}
    except Exception:
        return {}


def _save_json_dict(path: Path, data: Dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, indent=2) + "\n")


def _runtime_user_can_read(path: Path) -> bool:
    try:
        st = path.stat()
    except Exception:
        return False
    mode = stat.S_IMODE(st.st_mode)
    if st.st_uid == _OPENCLAW_RUNTIME_UID and mode & stat.S_IRUSR:
        return True
    if st.st_gid == _OPENCLAW_RUNTIME_GID and mode & stat.S_IRGRP:
        return True
    if mode & stat.S_IROTH:
        return True
    return False


def _runtime_user_can_enter_dir(path: Path) -> bool:
    try:
        st = path.stat()
    except Exception:
        return False
    mode = stat.S_IMODE(st.st_mode)
    if st.st_uid == _OPENCLAW_RUNTIME_UID and mode & stat.S_IXUSR and mode & stat.S_IRUSR:
        return True
    if st.st_gid == _OPENCLAW_RUNTIME_GID and mode & stat.S_IXGRP and mode & stat.S_IRGRP:
        return True
    if mode & stat.S_IXOTH and mode & stat.S_IROTH:
        return True
    return False


def _repair_openclaw_runtime_access(install_dir: Path, config_path: Path, runtime_env_path: Path) -> List[str]:
    issues: List[str] = []
    config_dir = install_dir / ".openclaw"
    workspace_dir = install_dir / "workspace"
    config_dir.mkdir(parents=True, exist_ok=True)
    workspace_dir.mkdir(parents=True, exist_ok=True)

    def _touch_permissions(path: Path, mode: int) -> None:
        try:
            os.chown(path, _OPENCLAW_RUNTIME_UID, _OPENCLAW_RUNTIME_GID)
        except Exception:
            pass
        try:
            path.chmod(mode)
        except Exception as exc:
            issues.append(f"Could not set permissions on {path.name}: {exc}")

    stale_config_workspace = config_dir / "workspace"

    for root_dir in (config_dir, workspace_dir):
        if root_dir.exists():
            _touch_permissions(root_dir, 0o755)
        for dirpath, dirnames, filenames in os.walk(root_dir):
            current_dir = Path(dirpath)
            if current_dir == config_dir and "workspace" in dirnames and stale_config_workspace != workspace_dir:
                dirnames[:] = [name for name in dirnames if name != "workspace"]
            _touch_permissions(current_dir, 0o755)
            for dirname in dirnames:
                _touch_permissions(current_dir / dirname, 0o755)
            for filename in filenames:
                file_path = current_dir / filename
                mode = 0o644
                if file_path == runtime_env_path:
                    mode = 0o644
                _touch_permissions(file_path, mode)

    for required_path in (config_path, runtime_env_path):
        if required_path.exists():
            _touch_permissions(required_path, 0o644)

    if not _runtime_user_can_enter_dir(config_dir):
        issues.append(f"OpenClaw runtime cannot enter config directory {config_dir}.")
    if config_path.exists() and not _runtime_user_can_read(config_path):
        issues.append(f"OpenClaw runtime cannot read {config_path.name}.")
    if runtime_env_path.exists() and not _runtime_user_can_read(runtime_env_path):
        issues.append(f"OpenClaw runtime cannot read {runtime_env_path.name}.")
    if not _runtime_user_can_enter_dir(workspace_dir):
        issues.append(f"OpenClaw runtime cannot access workspace directory {workspace_dir}.")
    return list(dict.fromkeys(issues))


def _find_compose_path(install_dir: Path) -> Path | None:
    for name in ("docker-compose.yml", "docker-compose.yaml"):
        candidate = install_dir / name
        if candidate.exists():
            return candidate
    return None


def _ensure_openclaw_runtime_host_alias(install_dir: Path) -> bool:
    compose_path = _find_compose_path(install_dir)
    if compose_path is None:
        return False
    try:
        import yaml
    except Exception:
        return False

    try:
        data = yaml.safe_load(compose_path.read_text()) or {}
    except Exception:
        return False
    services = data.get("services")
    if not isinstance(services, dict):
        return False

    changed = False
    alias_entry = f"{_OTEL_HOST_ALIAS}:host-gateway"
    for service_name in ("openclaw-gateway", "openclaw-cli"):
        service = services.get(service_name)
        if not isinstance(service, dict):
            continue
        if str(service.get("network_mode") or "").strip():
            if service.get("extra_hosts"):
                service.pop("extra_hosts", None)
                changed = True
            continue
        extra_hosts = service.get("extra_hosts")
        if not isinstance(extra_hosts, list):
            extra_hosts = []
        normalized = [str(item).strip() for item in extra_hosts if str(item).strip()]
        if alias_entry not in normalized:
            normalized.append(alias_entry)
            service["extra_hosts"] = normalized
            changed = True

    if changed:
        compose_path.write_text(yaml.safe_dump(data, sort_keys=False))
    return changed


def _collector_endpoint_base() -> str:
    cfg = ConfigManager()
    endpoint = str(cfg.get_key("ARMORER_OTEL_ENDPOINT") or "").strip()
    if endpoint:
        return endpoint
    port = str(cfg.get_key("ARMORER_OTEL_COLLECTOR_PORT") or _OTEL_DEFAULT_PORT).strip() or _OTEL_DEFAULT_PORT
    return f"http://127.0.0.1:{port}"


def _normalize_openclaw_otel_runtime_endpoint(endpoint: str) -> str:
    raw = str(endpoint or "").strip()
    if not raw:
        raw = f"http://127.0.0.1:{_OTEL_DEFAULT_PORT}"
    parsed = urlsplit(raw)
    hostname = (parsed.hostname or "").strip().lower()
    if hostname in {"127.0.0.1", "localhost", "::1"}:
        netloc_host = _OTEL_HOST_ALIAS
        if parsed.port:
            netloc_host = f"{netloc_host}:{parsed.port}"
        parsed = parsed._replace(netloc=netloc_host)
    return urlunsplit(parsed).rstrip("/")


def _apply_openclaw_otel_diagnostics(data: Dict[str, Any], app_name: str) -> None:
    diagnostics = data.get("diagnostics")
    if not isinstance(diagnostics, dict):
        diagnostics = {}
        data["diagnostics"] = diagnostics
    diagnostics["enabled"] = True
    diagnostics["otel"] = {
        "enabled": True,
        "endpoint": _normalize_openclaw_otel_runtime_endpoint(_collector_endpoint_base()),
        "protocol": "http/protobuf",
        "serviceName": str(app_name or "openclaw").strip() or "openclaw",
        "traces": True,
        "metrics": True,
        "logs": True,
    }

    plugins = data.get("plugins")
    if not isinstance(plugins, dict):
        plugins = {}
        data["plugins"] = plugins
    plugins["enabled"] = True
    entries = plugins.get("entries")
    if not isinstance(entries, dict):
        entries = {}
        plugins["entries"] = entries
    entry = entries.get("diagnostics-otel")
    if not isinstance(entry, dict):
        entry = {}
    entry["enabled"] = True
    entries["diagnostics-otel"] = entry


def load_openclaw_instance_config(install_dir: Path, app_state: Dict[str, Any] | None = None) -> OpenClawInstanceConfig:
    env = _load_env_map(install_dir / ".env")
    runtime_env = _load_env_map(install_dir / ".openclaw" / ".env")
    for key, value in runtime_env.items():
        env.setdefault(key, value)
    data = _load_json_dict(install_dir / ".openclaw" / "openclaw.json")
    state = app_state or {}

    access_mode = str(state.get("access_mode") or "local").strip().lower()
    if access_mode not in {"local", "remote_tailscale"}:
        access_mode = "local"
    gateway = data.get("gateway", {}) if isinstance(data.get("gateway"), dict) else {}
    trusted_proxies = gateway.get("trustedProxies", [])
    remote_host = str(state.get("remote_host") or "").strip()
    remote_magicdns_name = str(state.get("remote_magicdns_name") or "").strip()

    primary_provider = str(state.get("primary_provider") or "openai")
    default_model = str(state.get("default_model") or "gpt-4o")
    enabled_channels = list(state.get("enabled_channels") or [])
    enabled_skills = list(state.get("enabled_skills") or [])
    skill_settings = dict(state.get("skill_settings") or {})
    web_search_provider = str(state.get("web_search_provider") or "disabled")

    for provider, key in OPENCLAW_PROVIDER_ENV_KEYS.items():
        if env.get(key):
            primary_provider = str(state.get("primary_provider") or provider)
            break

    for channel, keys in _CHANNEL_ENV_KEYS.items():
        if any(env.get(key) for key in keys):
            if channel not in enabled_channels:
                enabled_channels.append(channel)

    armorer_cfg = data.get("armorer", {}) if isinstance(data.get("armorer"), dict) else {}
    agent_cfg = data.get("agent", {}) if isinstance(data.get("agent"), dict) else {}
    agents_cfg = data.get("agents", {}) if isinstance(data.get("agents"), dict) else {}
    defaults_cfg = agents_cfg.get("defaults", {}) if isinstance(agents_cfg.get("defaults"), dict) else {}
    defaults_model = defaults_cfg.get("model")
    skills_cfg = data.get("skills", {}) if isinstance(data.get("skills"), dict) else {}
    skill_entries = skills_cfg.get("entries", {}) if isinstance(skills_cfg.get("entries"), dict) else {}
    tools_cfg = data.get("tools", {}) if isinstance(data.get("tools"), dict) else {}
    web_cfg = tools_cfg.get("web", {}) if isinstance(tools_cfg.get("web"), dict) else {}
    web_search_cfg = web_cfg.get("search", {}) if isinstance(web_cfg.get("search"), dict) else {}
    configured_model = ""
    if isinstance(agent_cfg.get("model"), str):
        configured_model = str(agent_cfg.get("model") or "").strip()
    elif isinstance(defaults_model, str):
        configured_model = str(defaults_model or "").strip()
    elif isinstance(defaults_model, dict):
        configured_model = str(defaults_model.get("primary") or "").strip()
    if configured_model:
        configured_provider, configured_model_id = _split_qualified_model(configured_model)
        if configured_provider:
            primary_provider = configured_provider
        if configured_model_id:
            default_model = configured_model_id

    if web_search_provider == "disabled" and isinstance(armorer_cfg.get("webSearchProvider"), str):
        web_search_provider = str(armorer_cfg.get("webSearchProvider") or "disabled")
    if web_search_provider == "disabled" and web_search_cfg:
        if web_search_cfg.get("enabled") is False:
            web_search_provider = "disabled"
        else:
            web_search_provider = str(web_search_cfg.get("provider") or "brave")
    if not enabled_skills:
        enabled_skills = [
            str(skill_key)
            for skill_key, skill_entry in skill_entries.items()
            if isinstance(skill_entry, dict) and skill_entry.get("enabled") is not False
        ]
    if not skill_settings:
        for skill_key in enabled_skills:
            skill_entry = skill_entries.get(skill_key, {})
            if isinstance(skill_entry, dict):
                skill_settings[str(skill_key)] = skill_entry

    return OpenClawInstanceConfig(
        access_mode=access_mode,
        remote_host=remote_host,
        remote_magicdns_name=remote_magicdns_name,
        primary_provider=primary_provider,
        default_model=default_model,
        enabled_channels=enabled_channels,
        enabled_skills=enabled_skills,
        trusted_proxies=[str(v) for v in trusted_proxies] if isinstance(trusted_proxies, list) else [],
        skill_settings=skill_settings,
        web_search_provider=web_search_provider,
    )


def apply_openclaw_instance_config(
    install_dir: Path,
    config: OpenClawInstanceConfig,
    secrets_map: Dict[str, str] | None = None,
    managed_skill_keys: list[str] | None = None,
) -> Dict[str, Any]:
    secrets_map = secrets_map or {}
    ensure_openclaw_instance_ports(install_dir)

    env_path = install_dir / ".env"
    env = _load_env_map(env_path)
    env, _ = canonicalize_openclaw_mount_env_paths(install_dir, env)
    env["OPENCLAW_IMAGE"] = resolve_openclaw_runtime_image(env)
    env.setdefault("OPENCLAW_GATEWAY_TOKEN", secrets.token_hex(32))
    gateway_token = str(env.get("OPENCLAW_GATEWAY_TOKEN") or "").strip()
    if not _valid_gateway_token(gateway_token):
        gateway_token = secrets.token_hex(32)
        env["OPENCLAW_GATEWAY_TOKEN"] = gateway_token

    env["OPENCLAW_GATEWAY_BIND"] = "lan"

    provider_key = OPENCLAW_PROVIDER_ENV_KEYS.get(config.primary_provider)
    if provider_key and secrets_map.get(provider_key):
        env[provider_key] = secrets_map[provider_key]

    env["MODEL_PROVIDER"] = config.primary_provider
    env["Armorer_MODEL"] = config.default_model

    if config.primary_provider == "ollama":
        if secrets_map.get("OLLAMA_BASE_URL"):
            env["OLLAMA_BASE_URL"] = secrets_map["OLLAMA_BASE_URL"]
        env.setdefault("OLLAMA_API_KEY", "ollama-local")

    for channel in config.enabled_channels:
        for key in _CHANNEL_ENV_KEYS.get(channel, []):
            if secrets_map.get(key):
                env[key] = secrets_map[key]

    _save_env_map(env_path, env)

    runtime_env_updates: Dict[str, str] = {}
    runtime_env_managed_keys: List[str] = list(OPENCLAW_PROVIDER_ENV_KEYS.values()) + list(_WEB_ENV_KEYS.values())
    provider_key = OPENCLAW_PROVIDER_ENV_KEYS.get(config.primary_provider)
    if provider_key:
        provider_secret = str(secrets_map.get(provider_key) or env.get(provider_key) or "").strip()
        if provider_secret:
            runtime_env_updates[provider_key] = provider_secret
    if config.primary_provider == "ollama":
        runtime_env_managed_keys.extend(["OLLAMA_BASE_URL", "OLLAMA_API_KEY"])
        ollama_base = str(secrets_map.get("OLLAMA_BASE_URL") or env.get("OLLAMA_BASE_URL") or "").strip()
        ollama_key = str(env.get("OLLAMA_API_KEY") or "ollama-local").strip()
        if ollama_base:
            runtime_env_updates["OLLAMA_BASE_URL"] = ollama_base
        if ollama_key:
            runtime_env_updates["OLLAMA_API_KEY"] = ollama_key
    for channel in _CHANNEL_ENV_KEYS:
        for key_name in _CHANNEL_ENV_KEYS.get(channel, []):
            runtime_env_managed_keys.append(key_name)
            if channel in config.enabled_channels:
                channel_secret = str(secrets_map.get(key_name) or env.get(key_name) or "").strip()
                if channel_secret:
                    runtime_env_updates[key_name] = channel_secret
    web_env_key = _WEB_ENV_KEYS.get(config.web_search_provider)
    if web_env_key:
        web_secret = str(secrets_map.get(web_env_key) or env.get(web_env_key) or "").strip()
        if web_secret:
            runtime_env_updates[web_env_key] = web_secret
    runtime_env_path = install_dir / ".openclaw" / ".env"
    previous_runtime_env = runtime_env_path.read_text() if runtime_env_path.exists() else ""
    _sync_env_map(runtime_env_path, runtime_env_updates, runtime_env_managed_keys)
    current_runtime_env = runtime_env_path.read_text() if runtime_env_path.exists() else ""

    config_path = install_dir / ".openclaw" / "openclaw.json"
    previous_config_text = config_path.read_text() if config_path.exists() else ""
    data = _load_json_dict(config_path)
    gateway = data.setdefault("gateway", {})
    gateway["mode"] = "local"
    auth_cfg = gateway.get("auth")
    if not isinstance(auth_cfg, dict):
        auth_cfg = {}
    auth_cfg["mode"] = "token"
    auth_cfg["token"] = gateway_token
    gateway["auth"] = auth_cfg
    remote_cfg = gateway.get("remote")
    if not isinstance(remote_cfg, dict):
        remote_cfg = {}
    remote_cfg["token"] = gateway_token
    gateway["remote"] = remote_cfg
    control_ui = gateway.setdefault("controlUi", {})
    allowed_origins = list(derive_openclaw_control_ui_origins(install_dir))
    remote_magicdns_name = str(config.remote_magicdns_name or "").strip()
    if config.access_mode == "remote_tailscale" and remote_magicdns_name:
        secure_origin = f"https://{remote_magicdns_name}"
        if secure_origin not in allowed_origins:
            allowed_origins.append(secure_origin)
    control_ui["allowedOrigins"] = allowed_origins
    gateway.pop("trustedProxies", None)

    data.pop("agent", None)
    agents = data.setdefault("agents", {})
    defaults = agents.setdefault("defaults", {})
    defaults["model"] = {"primary": _qualified_model(config.primary_provider, config.default_model)}

    models = data.setdefault("models", {})
    providers = models.setdefault("providers", {})
    if config.primary_provider == "ollama":
        ollama_provider = providers.setdefault("ollama", {})
        ollama_provider["baseUrl"] = _normalize_ollama_runtime_url(
            secrets_map.get("OLLAMA_BASE_URL") or env.get("OLLAMA_BASE_URL") or ""
        )
        ollama_provider["api"] = "ollama"
        ollama_provider["apiKey"] = env.get("OLLAMA_API_KEY", "ollama-local")
        ollama_provider["models"] = [
            {
                "id": config.default_model,
                "name": config.default_model,
            }
        ]

    channels_cfg = data.get("channels")
    if not isinstance(channels_cfg, dict):
        channels_cfg = {}
        data["channels"] = channels_cfg
    managed_channels = set(_CHANNEL_ENV_KEYS.keys())
    selected_channels = set(config.enabled_channels)
    for channel in managed_channels:
        existing_channel_cfg = channels_cfg.get(channel)
        if channel in selected_channels:
            next_channel_cfg = dict(existing_channel_cfg) if isinstance(existing_channel_cfg, dict) else {}
            next_channel_cfg["enabled"] = True
            channels_cfg[channel] = next_channel_cfg
            continue
        if isinstance(existing_channel_cfg, dict):
            next_channel_cfg = dict(existing_channel_cfg)
            next_channel_cfg["enabled"] = False
            channels_cfg[channel] = next_channel_cfg

    skills = data.setdefault("skills", {})
    entries = skills.setdefault("entries", {})
    desired_skill_keys = set(config.enabled_skills) | set(config.skill_settings.keys())
    managed_skill_scope = set(managed_skill_keys or []) | desired_skill_keys
    for skill_key in managed_skill_scope:
        existing_entry = entries.get(skill_key, {}) if isinstance(entries.get(skill_key), dict) else {}
        next_entry = dict(existing_entry)
        next_entry["enabled"] = False
        if skill_key in config.skill_settings:
            desired = config.skill_settings[skill_key]
            next_entry.update(desired)
            next_entry["enabled"] = True
        entries[skill_key] = next_entry

    tools = data.setdefault("tools", {})
    web = tools.setdefault("web", {})
    search = web.setdefault("search", {})
    if config.web_search_provider == "disabled":
        search["enabled"] = False
        search.pop("provider", None)
        search.pop("apiKey", None)
        search.pop("perplexity", None)
        search.pop("gemini", None)
        search.pop("grok", None)
        search.pop("kimi", None)
    else:
        search["enabled"] = True
        search["provider"] = config.web_search_provider
        if config.web_search_provider == "brave" and secrets_map.get("BRAVE_API_KEY"):
            search["apiKey"] = secrets_map["BRAVE_API_KEY"]
        elif config.web_search_provider == "perplexity" and secrets_map.get("PERPLEXITY_API_KEY"):
            search.setdefault("perplexity", {})["apiKey"] = secrets_map["PERPLEXITY_API_KEY"]
        elif config.web_search_provider == "gemini" and secrets_map.get("GEMINI_API_KEY"):
            search.setdefault("gemini", {})["apiKey"] = secrets_map["GEMINI_API_KEY"]
        elif config.web_search_provider == "grok" and secrets_map.get("XAI_API_KEY"):
            search.setdefault("grok", {})["apiKey"] = secrets_map["XAI_API_KEY"]
        elif config.web_search_provider == "kimi":
            kimi_key = secrets_map.get("KIMI_API_KEY") or secrets_map.get("MOONSHOT_API_KEY")
            if kimi_key:
                search.setdefault("kimi", {})["apiKey"] = kimi_key

    data.pop("armorer", None)
    _apply_openclaw_otel_diagnostics(data, install_dir.name)

    _save_json_dict(config_path, data)
    ensure_openclaw_bootstrap_config(config_path, install_dir)
    current_config_text = config_path.read_text() if config_path.exists() else ""
    compose_changed = _ensure_openclaw_runtime_host_alias(install_dir)

    access_issues = _repair_openclaw_runtime_access(install_dir, config_path, runtime_env_path)

    return {
        "app_config_state": asdict(config),
        "managed_files": [".env", ".openclaw/.env", ".openclaw/openclaw.json"],
        "channels_enabled": list(config.enabled_channels),
        "skills_enabled": list(config.enabled_skills),
        "runtime_env_changed": previous_runtime_env != current_runtime_env,
        "bootstrap_config_changed": previous_config_text != current_config_text,
        "compose_changed": compose_changed,
        "runtime_access_issues": access_issues,
    }
