"""Config loader for per-sandbox Beaver runtime settings."""

from __future__ import annotations

import json
import os
import sys
from pathlib import Path
from typing import Any

from .schema import (
    AgentDefaultsConfig,
    AuthzConfig,
    BackendIdentityConfig,
    BeaverConfig,
    ChannelConfig,
    EmbeddingConfig,
    MCPServerConfig,
    ProviderConfig,
    ToolsConfig,
)

LOCAL_MCP_CATEGORIES: dict[str, dict[str, str]] = {
    "local_filesystem_mcp": {"category": "filesystem", "display_name": "个人智能体文件系统工具"},
    "local_runtime_mcp": {"category": "runtime", "display_name": "本地运行工具"},
    "local_memory_mcp": {"category": "memory", "display_name": "本地记忆工具"},
    "local_skills_mcp": {"category": "skills", "display_name": "本地技能工具"},
    "local_coordination_mcp": {"category": "coordination", "display_name": "本地协作工具"},
    "local_scheduler_mcp": {"category": "scheduler", "display_name": "本地定时工具"},
    "local_web_mcp": {"category": "web", "display_name": "本地联网工具"},
}


def default_config_path(*, workspace: str | Path | None = None) -> Path:
    """Resolve the default config path for a single-user sandbox instance.

    Priority:
    1. `BEAVER_CONFIG_PATH`
    2. `BEAVER_HOME/config.json`
    3. `<workspace>/.beaver/config.json`
    4. `./.beaver/config.json`
    """

    explicit = os.getenv("BEAVER_CONFIG_PATH")
    if explicit:
        return Path(explicit).expanduser()

    beaver_home = os.getenv("BEAVER_HOME")
    if beaver_home:
        return Path(beaver_home).expanduser() / "config.json"

    root = Path(workspace).expanduser() if workspace is not None else Path.cwd()
    return root / ".beaver" / "config.json"


def load_config(
    *,
    workspace: str | Path | None = None,
    config_path: str | Path | None = None,
) -> BeaverConfig:
    """Load backend config; missing config is treated as an empty config."""

    path = Path(config_path).expanduser() if config_path is not None else default_config_path(workspace=workspace)
    if not path.exists():
        return BeaverConfig(config_path=path)

    data = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(data, dict):
        raise ValueError(f"Beaver config must be a JSON object: {path}")

    return BeaverConfig(
        agents_defaults=_parse_agent_defaults(data),
        providers=_parse_providers(data.get("providers")),
        embedding=_parse_embedding(data),
        tools=_parse_tools(data.get("tools")),
        authz=_parse_authz(data.get("authz")),
        channels=_parse_channels(data.get("channels")),
        backend_identity=_parse_backend_identity(data.get("backend_identity") or data.get("backendIdentity")),
        config_path=path,
    )


def _parse_agent_defaults(data: dict[str, Any]) -> AgentDefaultsConfig:
    agents = _as_dict(data.get("agents"))
    defaults = _as_dict(agents.get("defaults"))
    return AgentDefaultsConfig(
        workspace=_string(defaults.get("workspace") or data.get("workspace")),
        model=_string(defaults.get("model") or data.get("model")),
        provider=_string(defaults.get("provider") or data.get("provider")),
        embedding_model=_string(defaults.get("embeddingModel") or defaults.get("embedding_model") or data.get("embeddingModel")),
        max_tokens=_int(_first_config_value(
            defaults.get("maxTokens"),
            defaults.get("max_tokens"),
            data.get("maxTokens"),
            data.get("max_tokens"),
        )),
        temperature=_float(_first_config_value(defaults.get("temperature"), data.get("temperature"))),
        max_context_messages=_int(
            defaults.get("maxContextMessages")
            or defaults.get("max_context_messages")
            or data.get("maxContextMessages")
            or data.get("max_context_messages")
        ),
        max_tool_iterations=_int(_first_config_value(
            defaults.get("maxToolIterations"),
            defaults.get("max_tool_iterations"),
            data.get("maxToolIterations"),
            data.get("max_tool_iterations"),
        )),
    )


def _parse_providers(raw: Any) -> dict[str, ProviderConfig]:
    providers: dict[str, ProviderConfig] = {}
    for name, payload in _as_dict(raw).items():
        if not isinstance(payload, dict):
            continue
        providers[str(name)] = ProviderConfig(
            api_key=_string(payload.get("apiKey") or payload.get("api_key")),
            api_base=_string(payload.get("apiBase") or payload.get("api_base") or payload.get("baseUrl") or payload.get("base_url")),
            extra_headers=_string_dict(payload.get("extraHeaders") or payload.get("extra_headers") or payload.get("headers")),
            request_timeout_seconds=_float(
                payload.get("requestTimeoutSeconds")
                or payload.get("request_timeout_seconds")
                or payload.get("timeout")
            ),
        )
    return providers


def _parse_embedding(data: dict[str, Any]) -> EmbeddingConfig:
    raw = _as_dict(data.get("embedding") or data.get("embeddings"))
    return EmbeddingConfig(
        provider=_string(raw.get("provider") or raw.get("provider_name")),
        model=_string(raw.get("model") or data.get("embeddingModel") or data.get("embedding_model")),
        api_key=_string(raw.get("apiKey") or raw.get("api_key")),
        api_base=_string(raw.get("apiBase") or raw.get("api_base") or raw.get("baseUrl") or raw.get("base_url")),
        extra_headers=_string_dict(raw.get("extraHeaders") or raw.get("extra_headers") or raw.get("headers")),
        request_timeout_seconds=_float(
            raw.get("requestTimeoutSeconds") or raw.get("request_timeout_seconds") or raw.get("timeout")
        ),
    )


def _parse_tools(raw: Any) -> ToolsConfig:
    data = _as_dict(raw)
    mcp_servers: dict[str, MCPServerConfig] = {}
    for server_id, payload in _as_dict(data.get("mcpServers") or data.get("mcp_servers")).items():
        if not isinstance(payload, dict):
            continue
        mcp_servers[str(server_id)] = MCPServerConfig(
            command=_string(payload.get("command")) or "",
            args=_string_list(payload.get("args")),
            env=_string_dict(payload.get("env")),
            url=_string(payload.get("url")) or "",
            headers=_string_dict(payload.get("headers")),
            auth_mode=(_string(payload.get("authMode") or payload.get("auth_mode")) or "none").lower(),
            auth_audience=_string(payload.get("authAudience") or payload.get("auth_audience")) or "",
            auth_scopes=_string_list(payload.get("authScopes") or payload.get("auth_scopes")),
            tool_timeout=int(_float(payload.get("toolTimeout") or payload.get("tool_timeout")) or 30),
            sensitive=_bool(payload.get("sensitive"), default=False),
            kind=(_string(payload.get("kind")) or ("local" if payload.get("command") else "online")).lower(),
            category=_string(payload.get("category")) or ("local" if payload.get("command") else "online"),
            managed=_bool(payload.get("managed"), default=False),
            display_name=_string(payload.get("displayName") or payload.get("display_name")) or "",
            source=_string(payload.get("source")) or "config",
        )
    for server_id, meta in LOCAL_MCP_CATEGORIES.items():
        if server_id in mcp_servers:
            continue
        mcp_servers[server_id] = MCPServerConfig(
            command=sys.executable or "python",
            args=["-m", "beaver.interfaces.mcp.tools_server", "--category", meta["category"]],
            env={},
            kind="local",
            category=meta["category"],
            managed=True,
            display_name=meta["display_name"],
            source="beaver-default",
            tool_timeout=60,
        )
    return ToolsConfig(
        restrict_to_workspace=_bool(
            data.get("restrictToWorkspace") if "restrictToWorkspace" in data else data.get("restrict_to_workspace"),
            default=True,
        ),
        mcp_servers=mcp_servers,
    )


def _parse_authz(raw: Any) -> AuthzConfig:
    data = _as_dict(raw)
    return AuthzConfig(
        enabled=_bool(data.get("enabled"), default=False),
        base_url=_string(data.get("baseUrl") or data.get("base_url")) or "",
        request_timeout_seconds=int(_float(data.get("requestTimeoutSeconds") or data.get("request_timeout_seconds")) or 10),
        outlook_mcp_url=_string(data.get("outlookMcpUrl") or data.get("outlook_mcp_url")) or "",
    )


def _parse_channels(raw: Any) -> dict[str, ChannelConfig]:
    channels: dict[str, ChannelConfig] = {}
    for channel_id, payload in _as_dict(raw).items():
        cleaned_id = str(channel_id).strip()
        if not cleaned_id:
            continue
        channels[cleaned_id] = _parse_channel_config(payload)
    return channels


def _parse_channel_config(payload: Any) -> ChannelConfig:
    data = _as_dict(payload)
    return ChannelConfig(
        enabled=_bool(data.get("enabled"), default=False),
        kind=_string(data.get("kind")) or "",
        mode=_string(data.get("mode")) or "webhook",
        account_id=_string(data.get("accountId") or data.get("account_id")) or "",
        display_name=_string(data.get("displayName") or data.get("display_name")) or "",
        config=_normalize_config_map(data.get("config")),
        secrets=_string_dict(data.get("secrets")),
    )


def _normalize_config_map(value: Any) -> dict[str, Any]:
    if not isinstance(value, dict):
        return {}
    return {
        _camel_to_snake_key(str(key)): item
        for key, item in value.items()
        if str(key).strip()
    }


def _camel_to_snake_key(value: str) -> str:
    result: list[str] = []
    for char in value:
        if char.isupper() and result:
            result.append("_")
        result.append(char.lower())
    return "".join(result)


def _parse_backend_identity(raw: Any) -> BackendIdentityConfig:
    data = _as_dict(raw)
    return BackendIdentityConfig(
        backend_id=_string(data.get("backendId") or data.get("backend_id")) or "",
        client_id=_string(data.get("clientId") or data.get("client_id")) or "",
        client_secret=_string(data.get("clientSecret") or data.get("client_secret")) or "",
        name=_string(data.get("name")) or "",
        public_base_url=_string(data.get("publicBaseUrl") or data.get("public_base_url")) or "",
    )


def _as_dict(value: Any) -> dict[str, Any]:
    return value if isinstance(value, dict) else {}


def _first_config_value(*values: Any) -> Any:
    for value in values:
        if value not in (None, ""):
            return value
    return None


def _string(value: Any) -> str | None:
    if value is None:
        return None
    value = str(value).strip()
    return value or None


def _string_dict(value: Any) -> dict[str, str]:
    if not isinstance(value, dict):
        return {}
    return {str(key): str(item) for key, item in value.items() if item is not None}


def _string_list(value: Any) -> list[str]:
    if not isinstance(value, list):
        return []
    return [str(item) for item in value if str(item).strip()]


def _float(value: Any) -> float | None:
    if value in (None, ""):
        return None
    return float(value)


def _int(value: Any) -> int | None:
    parsed = _float(value)
    if parsed is None:
        return None
    return int(parsed)


def _bool(value: Any, *, default: bool) -> bool:
    if isinstance(value, bool):
        return value
    if value in (None, ""):
        return default
    if isinstance(value, str):
        return value.strip().lower() in {"1", "true", "yes", "on"}
    return bool(value)
