from __future__ import annotations

from copy import deepcopy
from datetime import datetime, timezone
import json
import os
import re
import secrets
from pathlib import Path
from typing import Any


def repo_root() -> Path:
    return Path(__file__).resolve().parents[4]


def agents_root() -> Path:
    return repo_root() / "agents"


def default_openclaw_config_path() -> Path:
    raw = os.environ.get("OPENCLAW_CONFIG_PATH", "~/.openclaw/openclaw.json")
    return Path(os.path.expanduser(raw)).resolve()


def openclaw_root(config_path: Path) -> Path:
    return config_path.expanduser().resolve().parent


def load_json(path: Path) -> dict[str, Any]:
    with path.open("r", encoding="utf-8") as f:
        return json.load(f)


def dump_json(path: Path, data: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, ensure_ascii=False)
        f.write("\n")


def get_agent_config_path(agent_id: str) -> Path:
    return agents_root() / agent_id / ".config.json"


def candidate_agent_directories(source_path: Path, agent_id: str) -> list[Path]:
    candidates = [source_path / agent_id, source_path]
    unique: list[Path] = []
    seen: set[str] = set()
    for candidate in candidates:
        resolved = str(candidate)
        if resolved not in seen:
            unique.append(candidate)
            seen.add(resolved)
    return unique


def resolve_agent_directory(agent_id: str, source_path: Path | None = None) -> Path:
    if source_path is None:
        return get_agent_config_path(agent_id).parent

    resolved_source = source_path.expanduser().resolve()
    for candidate in candidate_agent_directories(resolved_source, agent_id):
        config_path = candidate / ".config.json"
        if not config_path.is_file():
            continue
        data = load_json(config_path)
        if data.get("agent_id") == agent_id:
            return candidate

    raise FileNotFoundError(f"未找到已下载的 Agent 配置: {resolved_source}")


def get_agent_config_path_from_source(agent_id: str, source_path: Path | None = None) -> Path:
    if source_path is None:
        return get_agent_config_path(agent_id)
    return resolve_agent_directory(agent_id, source_path) / ".config.json"


def load_agent_definition(agent_id: str, source_path: Path | None = None) -> dict[str, Any]:
    path = get_agent_config_path_from_source(agent_id, source_path)
    if not path.is_file():
        raise FileNotFoundError(f"未找到 Agent 配置: {path}")
    data = load_json(path)
    if data.get("agent_id") != agent_id:
        raise ValueError(f".config.json 中的 agent_id 与目录不一致: {path}")
    if not data.get("version"):
        raise ValueError(f"Agent 配置缺少 version: {path}")
    return data


def extract_placeholders(text: str) -> list[str]:
    return list(dict.fromkeys(re.findall(r"\{\{[^{}]+\}\}", text)))


def markdown_files_in_dir(directory: Path) -> list[Path]:
    if not directory.is_dir():
        return []
    return sorted(path for path in directory.iterdir() if path.is_file() and path.suffix == ".md")


def read_agent_placeholders(agent_id: str, source_path: Path | None = None) -> list[str]:
    agent_dir = resolve_agent_directory(agent_id, source_path)
    placeholders: list[str] = []
    for md_path in markdown_files_in_dir(agent_dir):
        try:
            text = md_path.read_text(encoding="utf-8")
        except OSError:
            continue
        placeholders.extend(extract_placeholders(text))
    return list(dict.fromkeys(placeholders))


def normalize_placeholder_catalog(raw: Any) -> list[dict[str, str]]:
    """解析 .config.json 中的 placeholder_catalog，约定见 AGENT-CONFIG.md。"""
    if not isinstance(raw, list):
        return []
    result: list[dict[str, str]] = []
    for item in raw:
        if not isinstance(item, dict):
            continue
        token = item.get("token")
        if not isinstance(token, str) or not token.strip():
            continue
        label = item.get("label") if isinstance(item.get("label"), str) else ""
        description = item.get("description") if isinstance(item.get("description"), str) else ""
        result.append({"token": token.strip(), "label": label.strip(), "description": description.strip()})
    return result


def merge_placeholder_catalog_with_detected(
    catalog: list[dict[str, str]],
    detected: list[str],
) -> list[dict[str, Any]]:
    """合并 placeholder_catalog 与各条 label/description，以及 .md 中扫描到的 {{...}}：便于安装流程展示与校验。"""
    by_token: dict[str, dict[str, str]] = {e["token"]: e for e in catalog}
    out: list[dict[str, Any]] = []
    seen: set[str] = set()
    for entry in catalog:
        tok = entry["token"]
        seen.add(tok)
        out.append(
            {
                **entry,
                "present_in_markdown": tok in detected,
            }
        )
    for tok in detected:
        if tok not in seen:
            out.append(
                {
                    "token": tok,
                    "label": "",
                    "description": "",
                    "present_in_markdown": True,
                    "undeclared_in_config": True,
                }
            )
            seen.add(tok)
    return out


def parse_version(version: str | None) -> tuple[int, ...]:
    if not version:
        return ()
    try:
        return tuple(int(part) for part in version.split("."))
    except ValueError:
        return ()


def normalize_skills(skills: Any) -> list[str]:
    if not isinstance(skills, list):
        return []

    result: list[str] = []
    for item in skills:
        if isinstance(item, str) and item:
            result.append(item)
        elif isinstance(item, dict):
            name = item.get("name")
            if isinstance(name, str) and name:
                result.append(name)

    return list(dict.fromkeys(result))


def get_auto_install_skills(agent_def: dict[str, Any]) -> list[dict[str, Any]]:
    """返回 .config.json 中 auto_install: true 的 skill 条目（保序、去重）。"""
    skills = agent_def.get("skills")
    if not isinstance(skills, list):
        return []

    result: list[dict[str, Any]] = []
    seen: set[str] = set()
    for item in skills:
        if not isinstance(item, dict):
            continue
        if item.get("auto_install") is not True:
            continue
        name = item.get("name")
        if not isinstance(name, str) or not name.strip():
            continue
        skill_name = name.strip()
        if skill_name in seen:
            continue
        seen.add(skill_name)
        result.append(item)
    return result


def normalize_dependencies(dependencies: Any) -> list[str]:
    if not isinstance(dependencies, list):
        return []
    result: list[str] = []
    for item in dependencies:
        if isinstance(item, str) and item:
            result.append(item)
    return list(dict.fromkeys(result))


def desired_workspace(agent_def: dict[str, Any], workspace_override: str | None = None) -> str:
    if isinstance(workspace_override, str) and workspace_override.strip():
        return workspace_override.strip()

    install = agent_def.get("install", {})
    workspace = install.get("workspace") or agent_def.get("workspace")
    if isinstance(workspace, str) and workspace:
        return workspace
    return f"/home/node/.openclaw/workspace-{agent_def['agent_id']}"


def get_agent_dependencies(agent_def: dict[str, Any]) -> list[str]:
    return normalize_dependencies(agent_def.get("agent_dependencies"))


def normalize_llm_primary(llm: Any) -> str | None:
    """将 .config.json 顶层 llm 转为 openclaw Agent model.primary；无 provider 时补 sophnet/。"""
    if not isinstance(llm, str):
        return None
    stripped = llm.strip()
    if not stripped:
        return None
    if "/" in stripped:
        return stripped
    return f"sophnet/{stripped}"


def normalize_heartbeat_for_openclaw(agent_def: dict[str, Any]) -> dict[str, Any] | None:
    """
    仅当 .config.json 显式配置 heartbeat 且包含有效 every 时返回配置块。
    未配置、空对象、或缺少 every 时返回 None（install 不写入 openclaw.json 的 heartbeat）。
    """
    raw = agent_def.get("heartbeat")
    if raw is None:
        return None
    if not isinstance(raw, dict) or not raw:
        return None
    every = raw.get("every")
    if not isinstance(every, str) or not every.strip():
        return None
    return raw


def build_agent_entry(
    agent_def: dict[str, Any],
    *,
    target_id_override: str | None = None,
    workspace_override: str | None = None,
    name_override: str | None = None,
) -> dict[str, Any]:
    source_agent_id = agent_def["agent_id"]
    agent_id = target_id_override.strip() if isinstance(target_id_override, str) and target_id_override.strip() else source_agent_id
    install = agent_def.get("install", {}) if isinstance(agent_def.get("install"), dict) else {}
    identity = install.get("identity", {}) if isinstance(install.get("identity"), dict) else {}
    resolved_name = name_override.strip() if isinstance(name_override, str) and name_override.strip() else None

    install_name_raw = install.get("name")
    install_name_str = install_name_raw.strip() if isinstance(install_name_raw, str) and install_name_raw.strip() else None

    identity_name_raw = identity.get("name")
    identity_name_str = identity_name_raw.strip() if isinstance(identity_name_raw, str) and identity_name_raw.strip() else None

    agent_label_raw = agent_def.get("agent_label")
    agent_label_str = agent_label_raw.strip() if isinstance(agent_label_raw, str) and agent_label_raw.strip() else None

    display_name = resolved_name or install_name_str or identity_name_str or agent_label_str or source_agent_id
    entry: dict[str, Any] = {
        "id": agent_id,
        "workspace": desired_workspace(agent_def, workspace_override),
        "identity": {
            "name": resolved_name or identity_name_str or agent_label_str or display_name,
            "theme": identity.get("theme", display_name),
            "emoji": identity.get("emoji", "🤖"),
        },
    }

    if resolved_name:
        entry["name"] = resolved_name
    elif install_name_str:
        entry["name"] = install_name_str

    if install.get("agent_dir"):
        entry["agentDir"] = install["agent_dir"]

    install_model = install.get("model")
    if isinstance(install_model, dict) and install_model:
        entry["model"] = install_model
    else:
        primary = normalize_llm_primary(agent_def.get("llm"))
        if primary is not None:
            entry["model"] = {"primary": primary}

    if isinstance(agent_def.get("tools"), dict):
        entry["tools"] = agent_def["tools"]

    skills = normalize_skills(agent_def.get("skills"))
    if skills:
        entry["skills"] = skills

    if install.get("subagents"):
        entry["subagents"] = install["subagents"]

    heartbeat = normalize_heartbeat_for_openclaw(agent_def)
    if heartbeat is not None:
        entry["heartbeat"] = heartbeat

    return entry


def build_bot_api_account(
    agent_def: dict[str, Any],
    *,
    agent_id_override: str | None = None,
    existing_secret: str | None = None,
    name_override: str | None = None,
) -> tuple[str, dict[str, Any]] | None:
    if not agent_def.get("bot_api_enabled"):
        return None

    install = agent_def.get("install", {}) if isinstance(agent_def.get("install"), dict) else {}
    identity = install.get("identity", {}) if isinstance(install.get("identity"), dict) else {}
    bot_api = install.get("bot_api", {}) if isinstance(install.get("bot_api"), dict) else {}
    resolved_name = name_override.strip() if isinstance(name_override, str) and name_override.strip() else None

    identity_name_raw = identity.get("name")
    identity_name_str = identity_name_raw.strip() if isinstance(identity_name_raw, str) and identity_name_raw.strip() else None
    agent_label_raw = agent_def.get("agent_label")
    agent_label_str = agent_label_raw.strip() if isinstance(agent_label_raw, str) and agent_label_raw.strip() else None

    source_agent_id = agent_def["agent_id"]
    agent_id = agent_id_override.strip() if isinstance(agent_id_override, str) and agent_id_override.strip() else source_agent_id
    account_id = bot_api.get("account_id", agent_id)
    account_name = bot_api.get("account_name") or resolved_name or identity_name_str or agent_label_str or source_agent_id
    enabled = bot_api.get("enabled", True)

    return account_id, {
        "agentId": agent_id,
        "name": account_name,
        "apiSecret": existing_secret or secrets.token_hex(32),
        "enabled": enabled,
    }


def load_openclaw_config(config_path: Path) -> dict[str, Any]:
    if not config_path.is_file():
        raise FileNotFoundError(f"未找到 openclaw.json: {config_path}")
    return load_json(config_path)


def agent_install_state_path(config_path: Path, agent_id: str) -> Path:
    return openclaw_root(config_path) / "agents" / agent_id / "install-state.json"


def load_install_state(config_path: Path, agent_id: str) -> dict[str, Any] | None:
    path = agent_install_state_path(config_path, agent_id)
    if not path.is_file():
        return None
    return load_json(path)


def write_install_state(config_path: Path, agent_id: str, data: dict[str, Any]) -> Path:
    path = agent_install_state_path(config_path, agent_id)
    payload = dict(data)
    payload["updatedAt"] = datetime.now(timezone.utc).isoformat()
    dump_json(path, payload)
    return path


def find_agent_entry(config: dict[str, Any], agent_id: str) -> dict[str, Any] | None:
    agents = config.get("agents", {}).get("list", [])
    for agent in agents:
        if isinstance(agent, dict) and agent.get("id") == agent_id:
            return agent
    return None


def agent_id_matches(entry_id: object, agent_id: str) -> bool:
    return isinstance(entry_id, str) and (entry_id == agent_id or entry_id.startswith(f"{agent_id}-"))


def read_source_agent_id_from_agent_dir(agent_entry: dict[str, Any]) -> str | None:
    agent_dir = agent_entry.get("agentDir")
    if not isinstance(agent_dir, str) or not agent_dir:
        return None

    config_path = Path(agent_dir) / ".config.json"
    if not config_path.is_file():
        return None

    try:
        data = load_json(config_path)
    except (OSError, json.JSONDecodeError):
        return None
    source_agent_id = data.get("agent_id")
    return source_agent_id if isinstance(source_agent_id, str) else None


def read_source_agent_id_from_install_state(config_path: Path, openclaw_id: str) -> str | None:
    state = load_install_state(config_path, openclaw_id)
    if not state:
        return None
    source_agent_id = state.get("agent_id")
    return source_agent_id if isinstance(source_agent_id, str) else None


def entry_matches_source_agent(config_path: Path, agent_entry: dict[str, Any], agent_id: str) -> bool:
    entry_id = agent_entry.get("id")
    if agent_id_matches(entry_id, agent_id):
        return True

    if isinstance(entry_id, str):
        source_agent_id = read_source_agent_id_from_install_state(config_path, entry_id)
        if source_agent_id == agent_id:
            return True

    source_agent_id = read_source_agent_id_from_agent_dir(agent_entry)
    return source_agent_id == agent_id


def is_agent_installed(config_path: Path, config: dict[str, Any], agent_id: str) -> bool:
    agents = config.get("agents", {}).get("list", [])
    for agent_entry in agents:
        if isinstance(agent_entry, dict) and entry_matches_source_agent(config_path, agent_entry, agent_id):
            return True
    return False


def resolve_existing_agent_entry(config_path: Path, config: dict[str, Any], agent_id: str, workspace: str) -> dict[str, Any] | None:
    entry_by_id = find_agent_entry(config, agent_id)
    if entry_by_id is not None:
        return entry_by_id
    entry_by_workspace = find_agent_by_workspace(config, workspace)
    if entry_by_workspace is not None and entry_matches_source_agent(config_path, entry_by_workspace, agent_id):
        return entry_by_workspace
    return None


def find_agent_by_workspace(config: dict[str, Any], workspace: str) -> dict[str, Any] | None:
    agents = config.get("agents", {}).get("list", [])
    for agent in agents:
        if isinstance(agent, dict) and agent.get("workspace") == workspace:
            return agent
    return None


def find_bot_api_accounts_by_agent_id(config: dict[str, Any], agent_id: str) -> dict[str, dict[str, Any]]:
    accounts = config.get("channels", {}).get("bot-api", {}).get("accounts", {})
    result: dict[str, dict[str, Any]] = {}
    for account_id, account in accounts.items():
        if isinstance(account, dict) and account.get("agentId") == agent_id:
            result[account_id] = account
    return result


def read_version_from_agent_dir(agent_entry: dict[str, Any]) -> str | None:
    agent_dir = agent_entry.get("agentDir")
    if not isinstance(agent_dir, str) or not agent_dir:
        return None

    config_path = Path(agent_dir) / ".config.json"
    if not config_path.is_file():
        return None

    try:
        data = load_json(config_path)
    except (OSError, json.JSONDecodeError):
        return None
    version = data.get("version")
    return version if isinstance(version, str) else None


def read_version_from_workspace_git_file(agent_entry: dict[str, Any]) -> str | None:
    workspace = agent_entry.get("workspace")
    if not isinstance(workspace, str) or not workspace:
        return None

    version_file = Path(workspace) / "git_version"
    if not version_file.is_file():
        return None

    try:
        return version_file.read_text(encoding="utf-8").strip() or None
    except OSError:
        return None


def detect_installed_version(config_path: Path, agent_id: str, agent_entry: dict[str, Any]) -> tuple[str | None, str | None]:
    version = read_version_from_agent_dir(agent_entry)
    if version:
        return version, "agent_dir_config"

    current_openclaw_id = agent_entry.get("id")
    state = None
    if isinstance(current_openclaw_id, str) and current_openclaw_id:
        state = load_install_state(config_path, current_openclaw_id)
        if state:
            state_version = state.get("version")
            if isinstance(state_version, str) and state_version:
                return state_version, "install_state_openclaw_id"

    if current_openclaw_id != agent_id:
        state = load_install_state(config_path, agent_id)
    else:
        state = load_install_state(config_path, agent_id)
    if state:
        state_version = state.get("version")
        if isinstance(state_version, str) and state_version:
            return state_version, "install_state_agent_id"

    version = read_version_from_workspace_git_file(agent_entry)
    if version:
        return version, "workspace_git_version"

    return None, None


def deep_merge_dict(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
    result = deepcopy(base)
    for key, value in override.items():
        if isinstance(value, dict) and isinstance(result.get(key), dict):
            result[key] = deep_merge_dict(result[key], value)
        else:
            result[key] = deepcopy(value)
    return result


def auto_generate_image_description_prefix(openclaw_agent_id: str) -> str:
    """与 openclaw.json 中 tools.media.image.scope.rules.match.keyPrefix 对齐。"""
    aid = openclaw_agent_id.strip() if isinstance(openclaw_agent_id, str) else ""
    return f"agent:{aid}:" if aid else ""


def is_auto_generate_image_description_enabled(agent_def: dict[str, Any]) -> bool:
    """未设置或为真时启用自动生成图片描述；仅当显式为 false 时关闭。"""
    raw = agent_def.get("auto_generate_image_description")
    if raw is None:
        return True
    if isinstance(raw, bool):
        return raw
    return bool(raw)


def _rule_is_auto_image_description_deny(rule: Any, key_prefix: str) -> bool:
    if not isinstance(rule, dict):
        return False
    if rule.get("action") != "deny":
        return False
    match = rule.get("match")
    if not isinstance(match, dict):
        return False
    return match.get("keyPrefix") == key_prefix


def sync_openclaw_auto_image_description_rules(
    config: dict[str, Any],
    openclaw_agent_id: str,
    *,
    enabled: bool,
) -> None:
    """
    同步根级 tools.media.image.scope.rules：
    - enabled=True：移除本安装流程写入的 deny（keyPrefix agent:<id>:）
    - enabled=False：确保存在对应 deny 规则（与产品侧「关闭自动生成图片描述」一致）
    """
    key_prefix = auto_generate_image_description_prefix(openclaw_agent_id)
    if not key_prefix:
        return

    tools = config.setdefault("tools", {})
    media = tools.setdefault("media", {})
    image = media.setdefault("image", {})
    scope = image.setdefault("scope", {})
    scope.setdefault("default", "allow")
    rules_any = scope.setdefault("rules", [])
    if not isinstance(rules_any, list):
        scope["rules"] = []
        rules_any = scope["rules"]

    if enabled:
        scope["rules"] = [r for r in rules_any if not _rule_is_auto_image_description_deny(r, key_prefix)]
        return

    if any(_rule_is_auto_image_description_deny(r, key_prefix) for r in rules_any):
        return

    rules_any.append(
        {
            "action": "deny",
            "match": {"keyPrefix": key_prefix},
        }
    )
