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 armorer.openclaw_config import (
    canonicalize_openclaw_mount_env_paths,
    derive_openclaw_control_ui_origins,
    ensure_openclaw_bootstrap_config,
    ensure_openclaw_instance_ports,
)


_PROVIDER_ENV_KEYS = {
    "openai": "OPENAI_API_KEY",
    "anthropic": "ANTHROPIC_API_KEY",
    "gemini": "GEMINI_API_KEY",
    "openrouter": "OPENROUTER_API_KEY",
}

_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


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"
    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}")

    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)
            _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 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")
    gateway = data.get("gateway", {}) if isinstance(data.get("gateway"), dict) else {}
    trusted_proxies = gateway.get("trustedProxies", [])
    if trusted_proxies and access_mode == "local":
        access_mode = "reverse_proxy"
    elif env.get("OPENCLAW_GATEWAY_BIND", "").strip().lower() not in {"", "local", "loopback", "127.0.0.1"}:
        if access_mode == "local":
            access_mode = "lan"

    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 _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,
        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.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

    bind_by_mode = {
        "local": "loopback",
        "lan": "lan",
        "reverse_proxy": "lan",
    }
    env["OPENCLAW_GATEWAY_BIND"] = bind_by_mode.get(config.access_mode, "loopback")

    provider_key = _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(_PROVIDER_ENV_KEYS.values()) + list(_WEB_ENV_KEYS.values())
    provider_key = _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", {})
    control_ui["allowedOrigins"] = derive_openclaw_control_ui_origins(install_dir)
    if config.access_mode == "reverse_proxy" and config.trusted_proxies:
        gateway["trustedProxies"] = config.trusted_proxies
    else:
        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)

    _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 ""

    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,
        "runtime_access_issues": access_issues,
    }
