from __future__ import annotations

from copy import deepcopy
from dataclasses import dataclass
from functools import lru_cache
from importlib import import_module
from pathlib import Path
from typing import Any, Callable

import yaml

DEFAULT_MODEL = "openai/gpt-4o-mini"
DEFAULT_NAME = "hyw"
CONFIG_PATH = Path.home() / ".hyw" / "config.yml"
LOG_DIR = Path.home() / ".hyw" / "logs"
_SUPPORTED_WIRE_APIS = {"chat", "completion", "responses"}
DEFAULT_CONFIG_TEMPLATE = """# Auto-generated by hyw on first launch.
# Fill in your real provider keys if needed.

# Shared provider defaults.
# `models[*]` and `sub_agent.*` inherit these unless they override them.
api_key: ""
api_base: ""

# Alternative provider-style defaults (YAML form).
# model_provider: mirror
# model_providers:
#   mirror:
#     base_url: https://chat.soruxgpt.com/codex
#     wire_api: responses
#     requires_openai_auth: true
#     custom_llm_provider: openai

models:
  - name: main
    model: openai/gpt-4o-mini

# Runtime options used by the app.
language: zh-CN
stream: true
headless: false
# Maximum main-loop rounds. Default is 15.
max_rounds: 15
# Appended to the main controller system prompt.
system_prompt: ""

# Legacy / compatibility child overrides.
# Kept for old configs; the current main flow does not dispatch these slots.
sub_agent:
  websearch:
    # Legacy compatibility slot.
    model: ""
    api_key: ""
    api_base: ""
  page:
    # Legacy compatibility slot.
    model: ""
    api_key: ""
    api_base: ""

# Tool capability registry + default provider selection.
tools:
  index:
    ddgs:
      search: core.search_ddgs:ddgs_search
    jina_ai:
      search: core.search_jina_ai:jina_ai_search
      page_extract: core.search_jina_ai:jina_ai_page_extract
    md2png_lite:
      render: md2png_lite.provider:render_md2png_lite_result
  use:
    search: ddgs
    page_extract: jina_ai
    render: md2png_lite

"""

_TOOL_CAPABILITIES = {"search", "page_extract", "render"}
_TOOL_PROVIDER_ALIASES = {
    "ddg": "ddgs",
    "duckduckgo": "ddgs",
    "jina": "jina_ai",
    "jina-ai": "jina_ai",
    "jinaai": "jina_ai",
    "md2png-lite": "md2png_lite",
}
_TOOL_CAPABILITY_ALIASES = {
    "extract": "page_extract",
    "page": "page_extract",
    "page_extract": "page_extract",
    "page-search": "page_extract",
    "page_search": "page_extract",
    "render": "render",
    "search": "search",
}

BUILTIN_TOOL_PROVIDER_INDEX: dict[str, dict[str, str]] = {
    "ddgs": {
        "search": "core.search_ddgs:ddgs_search",
    },
    "jina_ai": {
        "search": "core.search_jina_ai:jina_ai_search",
        "page_extract": "core.search_jina_ai:jina_ai_page_extract",
    },
    "md2png_lite": {
        "render": "md2png_lite.provider:render_md2png_lite_result",
    },
    "render": {
        "render": "md2png_lite.provider:render_md2png_lite_result",
    },
}

DEFAULT_TOOL_SELECTIONS: dict[str, str] = {
    "search": "ddgs",
    "page_extract": "jina_ai",
    "render": "md2png_lite",
}


@dataclass(frozen=True)
class ConfigFileState:
    path: Path
    exists: bool
    valid: bool
    raw: dict[str, Any]
    error: str | None = None


def ensure_config_file(path: str | Path | None = None) -> Path:
    target = Path(path or CONFIG_PATH).expanduser()
    if target.exists():
        return target
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text(DEFAULT_CONFIG_TEMPLATE, encoding="utf-8")
    return target


def _clean_cfg_text(value: Any) -> str:
    return str(value).strip() if value is not None else ""


def _clean_cfg_positive_int(value: Any) -> int | None:
    if value is None or isinstance(value, bool):
        return None
    if isinstance(value, int):
        return value if value > 0 else None
    if isinstance(value, float) and value.is_integer():
        parsed = int(value)
        return parsed if parsed > 0 else None

    text = _clean_cfg_text(value)
    if not text:
        return None
    if text.isdigit():
        parsed = int(text)
        return parsed if parsed > 0 else None
    return None


def _clean_cfg_bool(value: Any) -> bool | None:
    if isinstance(value, bool):
        return value
    text = _clean_cfg_text(value).lower()
    if not text:
        return None
    if text in {"1", "true", "yes", "on"}:
        return True
    if text in {"0", "false", "no", "off"}:
        return False
    return None


def _clean_cfg_mapping(value: Any) -> dict[str, Any] | None:
    return deepcopy(value) if isinstance(value, dict) else None


def _merge_cfg_mapping(
    base: dict[str, Any] | None,
    override: dict[str, Any] | None,
) -> dict[str, Any] | None:
    if base is None and override is None:
        return None
    if base is None:
        return deepcopy(override)
    if override is None:
        return deepcopy(base)

    merged = deepcopy(base)
    for key, value in override.items():
        existing = merged.get(key)
        if isinstance(existing, dict) and isinstance(value, dict):
            merged[key] = _merge_cfg_mapping(existing, value) or {}
        else:
            merged[key] = deepcopy(value)
    return merged


def _clean_cfg_wire_api(value: Any) -> str:
    text = _clean_cfg_text(value).lower()
    return text if text in _SUPPORTED_WIRE_APIS else ""


def _normalize_model_provider_entry(name: str, entry: Any) -> dict[str, Any] | None:
    if not isinstance(entry, dict):
        return None

    provider_name = _clean_cfg_text(name)
    alias = _clean_cfg_text(entry.get("name") or provider_name)
    api_key = _clean_cfg_text(entry.get("api_key"))
    api_key_env = _clean_cfg_text(entry.get("api_key_env"))
    custom_llm_provider = _clean_cfg_text(
        entry.get("custom_llm_provider")
        or entry.get("litellm_provider")
        or entry.get("provider_type")
    )
    requires_openai_auth = _clean_cfg_bool(entry.get("requires_openai_auth"))
    if requires_openai_auth and not api_key and not api_key_env:
        api_key_env = "OPENAI_API_KEY"
    if requires_openai_auth and not custom_llm_provider:
        custom_llm_provider = "openai"

    normalized: dict[str, Any] = {
        "model_provider": provider_name or alias,
        "name": alias or provider_name,
    }
    api_base = _clean_cfg_text(entry.get("api_base") or entry.get("base_url"))
    wire_api = _clean_cfg_wire_api(entry.get("wire_api"))
    reasoning_effort = _clean_cfg_text(entry.get("reasoning_effort"))
    max_tokens = _clean_cfg_positive_int(entry.get("max_tokens"))
    max_completion_tokens = _clean_cfg_positive_int(entry.get("max_completion_tokens"))
    extra_body = _clean_cfg_mapping(entry.get("extra_body"))

    if api_key:
        normalized["api_key"] = api_key
    if api_key_env:
        normalized["api_key_env"] = api_key_env
    if api_base:
        normalized["api_base"] = api_base
    if custom_llm_provider:
        normalized["custom_llm_provider"] = custom_llm_provider
    if wire_api:
        normalized["wire_api"] = wire_api
    if requires_openai_auth is not None:
        normalized["requires_openai_auth"] = requires_openai_auth
    if reasoning_effort:
        normalized["reasoning_effort"] = reasoning_effort
    if max_tokens is not None:
        normalized["max_tokens"] = max_tokens
    if max_completion_tokens is not None:
        normalized["max_completion_tokens"] = max_completion_tokens
    if extra_body:
        normalized["extra_body"] = extra_body
    return normalized


def _model_provider_registry(config: dict[str, Any]) -> dict[str, dict[str, Any]]:
    raw = config.get("model_providers")
    if not isinstance(raw, dict):
        return {}

    providers: dict[str, dict[str, Any]] = {}
    for name, entry in raw.items():
        normalized = _normalize_model_provider_entry(_clean_cfg_text(name), entry)
        if not normalized:
            continue
        provider_name = _clean_cfg_text(normalized.get("model_provider") or name)
        alias = _clean_cfg_text(normalized.get("name") or provider_name)
        if provider_name:
            providers[provider_name] = deepcopy(normalized)
        if alias and alias != provider_name:
            providers.setdefault(alias, deepcopy(normalized))
    return providers


def _split_provider_prefixed_model(
    model: str,
    provider_registry: dict[str, dict[str, Any]] | None = None,
) -> tuple[str, str]:
    text = _clean_cfg_text(model)
    if not text or "/" not in text:
        return "", text
    prefix, remainder = text.split("/", 1)
    provider_name = _clean_cfg_text(prefix)
    if not provider_name or not remainder:
        return "", text
    if provider_registry and provider_name in provider_registry:
        return provider_name, _clean_cfg_text(remainder)
    return "", text


def _merge_model_settings(
    base: dict[str, Any],
    override: dict[str, Any] | None,
) -> dict[str, Any]:
    merged = dict(base)
    if not isinstance(override, dict):
        return merged

    for key in (
        "model",
        "model_provider",
        "custom_llm_provider",
        "api_key",
        "api_key_env",
        "api_base",
        "wire_api",
        "reasoning_effort",
        "max_tokens",
        "max_completion_tokens",
        "requires_openai_auth",
    ):
        value = override.get(key)
        if value in (None, ""):
            continue
        merged[key] = deepcopy(value)

    extra_body = _merge_cfg_mapping(
        _clean_cfg_mapping(base.get("extra_body")),
        _clean_cfg_mapping(override.get("extra_body")),
    )
    if extra_body:
        merged["extra_body"] = extra_body
    return merged


def _reset_provider_transport(base: dict[str, Any]) -> dict[str, Any]:
    cleaned = dict(base)
    for key in (
        "model_provider",
        "custom_llm_provider",
        "api_key",
        "api_key_env",
        "api_base",
        "wire_api",
        "extra_body",
    ):
        cleaned.pop(key, None)
    return cleaned


def _model_defaults(config: dict[str, Any]) -> dict[str, Any]:
    provider_registry = _model_provider_registry(config)
    saved = config.get("_model_defaults")
    if isinstance(saved, dict):
        defaults = {
            "model": _clean_cfg_text(saved.get("model")),
            "model_provider": _clean_cfg_text(saved.get("model_provider") or saved.get("provider")),
            "custom_llm_provider": _clean_cfg_text(saved.get("custom_llm_provider") or saved.get("litellm_provider")),
            "api_key": _clean_cfg_text(saved.get("api_key")),
            "api_key_env": _clean_cfg_text(saved.get("api_key_env")),
            "api_base": _clean_cfg_text(saved.get("api_base") or saved.get("base_url")),
            "wire_api": _clean_cfg_wire_api(saved.get("wire_api")),
            "reasoning_effort": _clean_cfg_text(saved.get("reasoning_effort")),
            "max_tokens": _clean_cfg_positive_int(saved.get("max_tokens")),
            "max_completion_tokens": _clean_cfg_positive_int(saved.get("max_completion_tokens")),
            "extra_body": _clean_cfg_mapping(saved.get("extra_body")),
        }
    else:
        raw_model = _clean_cfg_text(config.get("model") or config.get("model_name"))
        inferred_provider, normalized_model = _split_provider_prefixed_model(raw_model, provider_registry)
        provider_name = _clean_cfg_text(config.get("model_provider") or config.get("provider") or inferred_provider)
        provider_defaults = deepcopy(provider_registry.get(provider_name) or {})

        max_tokens = _clean_cfg_positive_int(config.get("max_tokens"))
        if max_tokens is None:
            max_tokens = _clean_cfg_positive_int(provider_defaults.get("max_tokens"))
        max_completion_tokens = _clean_cfg_positive_int(config.get("max_completion_tokens"))
        if max_completion_tokens is None:
            max_completion_tokens = _clean_cfg_positive_int(provider_defaults.get("max_completion_tokens"))

        defaults = _merge_model_settings(
            {
                "model": normalized_model or raw_model or _clean_cfg_text(provider_defaults.get("model")),
                "model_provider": provider_name or _clean_cfg_text(provider_defaults.get("model_provider")),
                "custom_llm_provider": _clean_cfg_text(
                    config.get("custom_llm_provider")
                    or config.get("litellm_provider")
                    or provider_defaults.get("custom_llm_provider")
                ),
                "api_key": _clean_cfg_text(config.get("api_key") or provider_defaults.get("api_key")),
                "api_key_env": _clean_cfg_text(config.get("api_key_env") or provider_defaults.get("api_key_env")),
                "api_base": _clean_cfg_text(
                    config.get("api_base")
                    or config.get("base_url")
                    or provider_defaults.get("api_base")
                ),
                "wire_api": _clean_cfg_wire_api(config.get("wire_api") or provider_defaults.get("wire_api")),
                "reasoning_effort": _clean_cfg_text(
                    config.get("reasoning_effort") or provider_defaults.get("reasoning_effort")
                ),
                "max_tokens": max_tokens,
                "max_completion_tokens": max_completion_tokens,
                "extra_body": _merge_cfg_mapping(
                    _clean_cfg_mapping(provider_defaults.get("extra_body")),
                    _clean_cfg_mapping(config.get("extra_body")),
                ),
            },
            {"model": normalized_model},
        )
    if not defaults["model"]:
        defaults["model"] = DEFAULT_MODEL
    if not defaults.get("model_provider"):
        defaults.pop("model_provider", None)
    if not defaults.get("custom_llm_provider"):
        defaults.pop("custom_llm_provider", None)
    if not defaults.get("api_key_env"):
        defaults.pop("api_key_env", None)
    if not defaults.get("wire_api"):
        defaults.pop("wire_api", None)
    return defaults


def _normalize_model_profile(
    entry: Any,
    *,
    defaults: dict[str, Any],
    provider_registry: dict[str, dict[str, Any]] | None = None,
    name_hint: str = "",
) -> dict[str, Any] | None:
    if isinstance(entry, str):
        entry = {"model": entry}
    if not isinstance(entry, dict):
        return None

    raw_model = _clean_cfg_text(entry.get("model") or entry.get("model_name") or defaults.get("model"))
    inferred_provider, normalized_model = _split_provider_prefixed_model(raw_model, provider_registry)
    provider_name = _clean_cfg_text(
        entry.get("model_provider")
        or entry.get("provider")
        or inferred_provider
        or defaults.get("model_provider")
    )
    provider_defaults = deepcopy((provider_registry or {}).get(provider_name) or {})
    normalized_defaults = dict(defaults)
    default_provider_name = _clean_cfg_text(defaults.get("model_provider"))
    if provider_name and default_provider_name and provider_name != default_provider_name:
        normalized_defaults = _reset_provider_transport(normalized_defaults)
    normalized_defaults = _merge_model_settings(normalized_defaults, provider_defaults)
    if provider_name:
        normalized_defaults["model_provider"] = provider_name

    model = normalized_model or _clean_cfg_text(normalized_defaults.get("model"))
    if not model:
        model = DEFAULT_MODEL

    profile: dict[str, Any] = {"model": model}
    alias = _clean_cfg_text(entry.get("name") or entry.get("label") or name_hint)
    if alias:
        profile["name"] = alias
    if provider_name:
        profile["model_provider"] = provider_name

    custom_llm_provider = _clean_cfg_text(
        entry.get("custom_llm_provider")
        or entry.get("litellm_provider")
        or normalized_defaults.get("custom_llm_provider")
    )
    api_key = _clean_cfg_text(entry.get("api_key") or normalized_defaults.get("api_key"))
    api_key_env = _clean_cfg_text(entry.get("api_key_env") or normalized_defaults.get("api_key_env"))
    api_base = _clean_cfg_text(entry.get("api_base") or entry.get("base_url") or normalized_defaults.get("api_base"))
    wire_api = _clean_cfg_wire_api(entry.get("wire_api") or normalized_defaults.get("wire_api"))
    reasoning_effort = _clean_cfg_text(entry.get("reasoning_effort") or normalized_defaults.get("reasoning_effort"))
    max_tokens = _clean_cfg_positive_int(entry.get("max_tokens"))
    if max_tokens is None:
        max_tokens = _clean_cfg_positive_int(normalized_defaults.get("max_tokens"))
    max_completion_tokens = _clean_cfg_positive_int(entry.get("max_completion_tokens"))
    if max_completion_tokens is None:
        max_completion_tokens = _clean_cfg_positive_int(normalized_defaults.get("max_completion_tokens"))
    extra_body = _merge_cfg_mapping(
        _clean_cfg_mapping(normalized_defaults.get("extra_body")),
        _clean_cfg_mapping(entry.get("extra_body")),
    )

    if custom_llm_provider:
        profile["custom_llm_provider"] = custom_llm_provider
    if api_key:
        profile["api_key"] = api_key
    if api_key_env:
        profile["api_key_env"] = api_key_env
    if api_base:
        profile["api_base"] = api_base
    if wire_api:
        profile["wire_api"] = wire_api
    if reasoning_effort:
        profile["reasoning_effort"] = reasoning_effort
    if max_tokens is not None:
        profile["max_tokens"] = max_tokens
    if max_completion_tokens is not None:
        profile["max_completion_tokens"] = max_completion_tokens
    if extra_body:
        profile["extra_body"] = extra_body
    return profile


_MODEL_PROFILE_KEYS = {
    "model",
    "model_name",
    "name",
    "label",
    "model_provider",
    "provider",
    "custom_llm_provider",
    "litellm_provider",
    "api_key",
    "api_key_env",
    "api_base",
    "base_url",
    "wire_api",
    "requires_openai_auth",
    "reasoning_effort",
    "max_tokens",
    "max_completion_tokens",
    "extra_body",
}


def _looks_like_single_model_profile(entry: Any) -> bool:
    if not isinstance(entry, dict):
        return False
    return any(str(key or "").strip() in _MODEL_PROFILE_KEYS for key in entry.keys())


def _normalize_models(config: dict[str, Any]) -> list[dict[str, Any]]:
    defaults = _model_defaults(config)
    provider_registry = _model_provider_registry(config)
    raw_models = config.get("models")
    profiles: list[dict[str, Any]] = []

    if isinstance(raw_models, list):
        for entry in raw_models:
            profile = _normalize_model_profile(entry, defaults=defaults, provider_registry=provider_registry)
            if profile:
                profiles.append(profile)
    elif isinstance(raw_models, dict):
        if _looks_like_single_model_profile(raw_models):
            profile = _normalize_model_profile(raw_models, defaults=defaults, provider_registry=provider_registry)
            if profile:
                profiles.append(profile)
        else:
            for name, entry in raw_models.items():
                profile = _normalize_model_profile(
                    entry,
                    defaults=defaults,
                    provider_registry=provider_registry,
                    name_hint=_clean_cfg_text(name),
                )
                if profile:
                    profiles.append(profile)

    if not profiles:
        fallback = _normalize_model_profile(config, defaults=defaults, provider_registry=provider_registry)
        if fallback:
            profiles.append(fallback)

    return profiles or [{"model": DEFAULT_MODEL}]


def _pick_model_index(config: dict[str, Any], key: str, profile_count: int, default: int) -> int:
    if profile_count <= 0:
        return 0
    raw_index = config.get(key)
    if isinstance(raw_index, str) and raw_index.strip().isdigit():
        raw_index = int(raw_index.strip())
    if isinstance(raw_index, int):
        return raw_index % profile_count
    return default % profile_count


def _normalize_tool_capability(value: str) -> str:
    raw = _clean_cfg_text(value).lower()
    normalized = _TOOL_CAPABILITY_ALIASES.get(raw, raw)
    if normalized not in _TOOL_CAPABILITIES:
        raise ValueError(f"unsupported tool capability: {value}")
    return normalized


def normalize_tool_provider_name(value: str) -> str:
    raw = _clean_cfg_text(value).lower()
    return _TOOL_PROVIDER_ALIASES.get(raw, raw)


def cfg_get(config: dict[str, Any], path: str, default: Any = None) -> Any:
    cur: Any = config
    for key in path.split("."):
        if not isinstance(cur, dict) or key not in cur:
            return default
        cur = cur[key]
    return cur if cur is not None else default


def _flatten_provider_tokens(raw: Any) -> list[str]:
    if raw is None:
        return []
    if isinstance(raw, str):
        text = raw.strip()
        if not text:
            return []
        if "," in text:
            return [part.strip() for part in text.split(",") if part.strip()]
        return [text]
    if isinstance(raw, (list, tuple, set)):
        items: list[str] = []
        for item in raw:
            items.extend(_flatten_provider_tokens(item))
        return items
    return []


def _selection_candidates(config: dict[str, Any], capability: str) -> list[Any]:
    tool_node = cfg_get(config, f"tools.{capability}")
    return [
        cfg_get(config, f"tools.use.{capability}"),
        cfg_get(config, f"tools.{capability}.providers"),
        cfg_get(config, f"tools.{capability}.provider"),
        tool_node,
        config.get(f"{capability}_providers"),
        config.get(f"{capability}_provider"),
    ]


def get_tool_provider_index(config: dict[str, Any] | None = None) -> dict[str, dict[str, str]]:
    index = {
        provider: dict(capabilities)
        for provider, capabilities in BUILTIN_TOOL_PROVIDER_INDEX.items()
    }
    cfg = config or {}
    custom_index = cfg_get(cfg, "tools.index", cfg_get(cfg, "tools.providers", {}))
    if not isinstance(custom_index, dict):
        return index

    for provider_name, mapping in custom_index.items():
        normalized_provider = normalize_tool_provider_name(str(provider_name))
        if not isinstance(mapping, dict):
            continue
        slot = index.setdefault(normalized_provider, {})
        for capability_name, target in mapping.items():
            if target is None:
                continue
            try:
                capability = _normalize_tool_capability(str(capability_name))
            except ValueError:
                continue
            target_text = _clean_cfg_text(target)
            if target_text:
                slot[capability] = target_text
    return index


def get_tool_provider_name(
    config: dict[str, Any] | None,
    capability: str,
    selection: Any = None,
) -> str:
    names = get_tool_provider_names(config, capability, selection=selection)
    return names[0] if names else str(DEFAULT_TOOL_SELECTIONS.get(_normalize_tool_capability(capability), "") or "")


def get_tool_provider_names(
    config: dict[str, Any] | None,
    capability: str,
    selection: Any = None,
) -> list[str]:
    normalized_capability = _normalize_tool_capability(capability)
    if selection is not None:
        raw_items = _flatten_provider_tokens(selection)
    else:
        raw_items = []
        for candidate in _selection_candidates(config or {}, normalized_capability):
            if isinstance(candidate, dict):
                raw_items = _flatten_provider_tokens(
                    candidate.get("providers")
                    or candidate.get("provider")
                    or candidate.get("use")
                )
            else:
                raw_items = _flatten_provider_tokens(candidate)
            if raw_items:
                break

    normalized = [
        item if ":" in item else normalize_tool_provider_name(item)
        for item in raw_items
        if _clean_cfg_text(item)
    ]
    if normalized:
        deduped: list[str] = []
        seen: set[str] = set()
        for item in normalized:
            if item in seen:
                continue
            seen.add(item)
            deduped.append(item)
        return deduped
    default = str(DEFAULT_TOOL_SELECTIONS.get(normalized_capability, "") or "")
    return [default] if default else []


@dataclass(frozen=True)
class ToolHandler:
    provider: str
    capability: str
    target: str
    callable: Callable[..., Any]


@lru_cache(maxsize=64)
def _load_callable(target: str) -> Callable[..., Any]:
    module_name, _, attr_name = str(target).partition(":")
    if not module_name or not attr_name:
        raise ValueError(f"invalid tool target: {target}")
    module = import_module(module_name)
    fn = getattr(module, attr_name, None)
    if not callable(fn):
        raise TypeError(f"target is not callable: {target}")
    return fn


def resolve_tool_handlers(
    config: dict[str, Any] | None,
    capability: str,
    selection: Any = None,
) -> list[ToolHandler]:
    normalized_capability = _normalize_tool_capability(capability)
    index = get_tool_provider_index(config)
    handlers: list[ToolHandler] = []
    seen: set[tuple[str, str]] = set()
    for name in get_tool_provider_names(config, normalized_capability, selection=selection):
        if not name:
            continue
        if ":" in name:
            provider = name
            target = name
        else:
            provider = normalize_tool_provider_name(name)
            target = _clean_cfg_text(index.get(provider, {}).get(normalized_capability))
            if not target:
                continue
        signature = (provider, target)
        if signature in seen:
            continue
        seen.add(signature)
        handlers.append(
            ToolHandler(
                provider=provider,
                capability=normalized_capability,
                target=target,
                callable=_load_callable(target),
            )
        )
    return handlers


def build_model_config(config: dict[str, Any] | None = None, model_index: int | None = None) -> dict[str, Any]:
    raw = dict(config or {})
    profiles = _normalize_models(raw)
    profile_count = len(profiles)
    index = (
        _pick_model_index(raw, "stage1_model_index", profile_count, 0)
        if model_index is None
        else (int(model_index or 0) % profile_count if profile_count > 0 else 0)
    )
    stage2_index = _pick_model_index(raw, "stage2_model_index", profile_count, 1 if profile_count > 1 else 0)

    active = profiles[index]
    cfg = dict(raw)
    cfg["models"] = profiles
    cfg["_model_defaults"] = _model_defaults(raw)
    cfg["stage1_model_index"] = index
    cfg["stage2_model_index"] = stage2_index
    cfg["model"] = active.get("model") or DEFAULT_MODEL

    for key in (
        "model_provider",
        "custom_llm_provider",
        "api_key",
        "api_key_env",
        "api_base",
        "wire_api",
        "reasoning_effort",
        "max_tokens",
        "max_completion_tokens",
    ):
        if key in active:
            cfg[key] = active[key]
        else:
            cfg.pop(key, None)
    if "extra_body" in active:
        cfg["extra_body"] = deepcopy(active["extra_body"])
    else:
        cfg.pop("extra_body", None)
    return cfg


def get_model_profiles(config: dict[str, Any] | None = None) -> list[dict[str, Any]]:
    return [dict(profile) for profile in _normalize_models(dict(config or {}))]


def inspect_config_file(path: str | Path | None = None) -> ConfigFileState:
    target = Path(path or CONFIG_PATH).expanduser()
    if not target.exists():
        return ConfigFileState(path=target, exists=False, valid=False, raw={})

    try:
        parsed = yaml.safe_load(target.read_text("utf-8"))
    except Exception as exc:
        return ConfigFileState(
            path=target,
            exists=True,
            valid=False,
            raw={},
            error=str(exc).strip() or exc.__class__.__name__,
        )

    if parsed is None:
        parsed = {}
    if not isinstance(parsed, dict):
        return ConfigFileState(
            path=target,
            exists=True,
            valid=False,
            raw={},
            error=f"配置文件根节点必须是对象，当前是 {type(parsed).__name__}",
        )

    return ConfigFileState(path=target, exists=True, valid=True, raw=parsed)


def load_config() -> dict[str, Any]:
    ensure_config_file()
    state = inspect_config_file()
    cfg = build_model_config(state.raw if state.valid else {})
    cfg["_config_path"] = str(state.path)
    cfg["_config_exists"] = state.exists
    cfg["_config_valid"] = state.valid
    if state.error:
        cfg["_config_error"] = state.error
    return cfg


__all__ = [
    "BUILTIN_TOOL_PROVIDER_INDEX",
    "CONFIG_PATH",
    "ConfigFileState",
    "DEFAULT_CONFIG_TEMPLATE",
    "DEFAULT_MODEL",
    "DEFAULT_NAME",
    "DEFAULT_TOOL_SELECTIONS",
    "LOG_DIR",
    "ToolHandler",
    "build_model_config",
    "cfg_get",
    "ensure_config_file",
    "get_model_profiles",
    "get_tool_provider_index",
    "get_tool_provider_name",
    "get_tool_provider_names",
    "inspect_config_file",
    "load_config",
    "normalize_tool_provider_name",
    "resolve_tool_handlers",
]
