"""配置管理 — 三层合并：默认值 → ~/.claude-index1/config.yaml → 项目 .index1.yaml"""

from __future__ import annotations

import os
from dataclasses import dataclass, field, fields
from pathlib import Path

import yaml

# 全局配置目录（支持 INDEX1_HOME 环境变量覆盖）
INDEX1_HOME = Path(os.environ["INDEX1_HOME"]) if os.environ.get("INDEX1_HOME") else Path.home() / ".claude-index1"
GLOBAL_CONFIG_FILE = INDEX1_HOME / "config.yaml"
PROJECT_CONFIG_FILE = ".index1.yaml"

# 预定义模型档位
# 后端预设模板
BACKEND_PRESETS: dict[str, dict[str, str | int]] = {
    "ollama": {
        "embed_api": "ollama",
        "ollama_url": "http://localhost:11434",
        "desc": "Ollama（推荐，开箱即用）",
    },
    "llama-cpp": {
        "embed_api": "openai_compat",
        "ollama_url": "http://localhost:8080",
        "desc": "llama.cpp server（轻量，直接加载 GGUF 模型）",
    },
    "lm-studio": {
        "embed_api": "openai_compat",
        "ollama_url": "http://localhost:1234",
        "desc": "LM Studio（GUI 桌面应用）",
    },
}

# 预定义模型档位
MODEL_PROFILES: dict[str, dict[str, str | int]] = {
    "lightweight": {
        "model": "all-minilm",
        "dim": 384,
        "desc": "轻量级，英文为主，~45MB 磁盘 / ~250MB 内存",
    },
    "standard": {
        "model": "nomic-embed-text",
        "dim": 768,
        "desc": "推荐默认，中英文通用，~270MB 磁盘 / ~500MB 内存",
    },
    "high_precision": {
        "model": "nomic-embed-text-v2-moe",
        "dim": 768,
        "desc": "高精度多语言，~1GB 磁盘 / ~1GB 内存",
    },
    "chinese": {
        "model": "bge-m3",
        "dim": 1024,
        "desc": "中文最优，支持 100+ 语言，~1.2GB 磁盘 / ~1.2GB 内存",
    },
    "multilingual": {
        "model": "paraphrase-multilingual",
        "dim": 768,
        "desc": "多语言通用（50+ 语言），中日韩英均衡，~1GB 磁盘 / ~1GB 内存",
    },
}


@dataclass
class Config:
    """index1 核心配置"""

    db_path: str = ""  # 空字符串表示使用默认路径
    embedding_model: str = "nomic-embed-text"
    embedding_dim: int = 768
    ollama_url: str = "http://localhost:11434"
    embed_api: str = "ollama"  # "ollama" | "openai_compat"
    embed_backend: str = "onnx"  # "onnx" | "ollama" — ONNX 默认，无需外部服务
    onnx_model: str = "BAAI/bge-small-en-v1.5"  # 默认 ONNX 模型
    onnx_dim: int = 384  # 默认 ONNX 模型维度
    top_k: int = 10
    rrf_k: int = 60
    watch_paths: list[str] = field(default_factory=list)
    watcher_auto_start: bool = True
    watcher_stale_threshold: int = 3600  # 秒，corpus 过期阈值
    collection: str = "default"
    rerank_enabled: bool = False
    rerank_model: str = "Xenova/ms-marco-MiniLM-L-6-v2"

    # ── 搜索质量门控 ──
    min_score_ratio: float = 0.15  # 相对质量门控：低于 top_score * ratio 的结果不返回

    # ── FTS5 列权重 — content, section, parent_section, tags ──
    fts5_column_weights: tuple[float, ...] = (1.0, 3.0, 1.5, 2.0)

    # ── Query Expansion 配置 ──
    expansion_enabled: bool = False  # LLM 查询扩展（默认关闭：测试显示当前扩展质量降低搜索精度，见 #152）
    expansion_score_decay: float = 0.8  # 扩展词补充搜索结果的 score 衰减系数

    # ── Engagement Boost 配置（#190）──
    engagement_boost_enabled: bool = False  # 总开关（默认关闭，config engagement_boost_enabled true 启用）
    time_decay_weight: float = 0.10  # P2-1: 时间衰减权重
    time_decay_halflife: int = 180  # P2-1: 半衰期（天）
    access_rate_weight: float = 0.08  # P2-2: 归一化访问率权重
    return_noise_weight: float = 0.05  # P2-3: 负反馈降噪权重（应用时取负）
    return_noise_threshold: float = 0.5  # P2-3: adoption ratio 阈值
    intent_match_weight: float = 0.05  # P2-5: 意图-类型匹配权重
    proximity_enabled: bool = False  # P2-4: FTS5 NEAR() 邻近度搜索
    proximity_max_distance: int = 5  # P2-4: NEAR() 最大词距

    # ── P3-2 Dynamic top_k（#191）──
    adaptive_top_k_enabled: bool = False  # 按查询类型自动调整返回数量
    adaptive_top_k_min: int = 3  # symbol_lookup 等精确查询的最小 top_k
    adaptive_top_k_max: int = 20  # 长查询/overview 的最大 top_k

    # ── P3-1 Causal Chain Expansion（#191）──
    causal_expansion_enabled: bool = False  # 因果链 1-hop 扩展搜索
    causal_expansion_max: int = 3  # 每次搜索最多扩展条数
    causal_expansion_min_confidence: float = 0.6  # fact_edges 最低置信度

    # ── P3-9 Session Continuity Boost（#191）──
    session_continuity_enabled: bool = False  # 多轮搜索连续性 boost
    session_continuity_weight: float = 0.10  # boost 权重（乘法）

    # ── P3-5 Returned Results Demotion（#191）──
    returned_demotion_enabled: bool = False  # 已返回结果降权
    returned_demotion_decay: float = 0.5  # 降权衰减因子

    # ── P3-6 Eager Embedding（#191）──
    eager_embed_enabled: bool = False  # observe 阶段高 importance fact 同步嵌入
    eager_embed_min_importance: float = 0.7  # 触发阈值（error=0.9/test=0.8/new_file=0.7）

    # ── P3-4 Result Grouping（#191）──
    result_grouping_enabled: bool = False  # 按文件聚合搜索结果

    # ── P3-3 Cross-Type Association（#191）──
    cross_association_enabled: bool = False  # fact↔code 跨类型关联
    cross_association_max: int = 2  # 最多补充条数

    # ── Web UI 配置 ──
    web_auto_start: bool = True  # SessionStart 时自动启动 Web UI 后台服务
    web_port: int = 6888  # Web UI 监听端口

    # ── Code Graph 配置 ──
    graph_enabled: bool = True  # 搜索时是否启用代码图增强
    graph_weight_semantic: float = 0.6
    graph_weight_pagerank: float = 0.2
    graph_weight_structural: float = 0.2
    graph_hop: int = 2
    graph_min_confidence: float = 0.5

    # ── Cognitive 配置 ──
    cognitive_enabled: bool = True
    cognitive_db_path: str = ""  # 空 = INDEX1_HOME / "cognitive.db"
    cognition_model: str = "llama3.2:3b"  # 维护用 chat 模型
    cognitive_decay_days: int = 7
    cognitive_gc_days: int = 90
    cognitive_gc_min_confidence: float = 0.1
    decay_beta: float = 0.0  # 0=对数衰减(默认), >0=幂律衰减 (1+n)^beta

    # ── Worldview Extraction 配置（#149）──
    worldview_extraction_enabled: bool = True  # 从 rules/skills 文件提取用户偏好
    user_config_paths: list[str] = field(
        default_factory=lambda: ["~/.claude/rules", "~/.claude/skills"],
    )

    # ── Observer 配置 ──
    observer_enabled: bool = True  # SessionEnd 时用 LLM 回顾 session
    anthropic_api_key: str = ""  # Claude API key（L2 observer）
    observer_claude_model: str = ""  # 覆盖 Claude observer 模型（空=用 OBSERVER_MODELS 默认值）
    gemini_api_key: str = ""  # Gemini API key（环境变量 GEMINI_API_KEY）
    openrouter_api_key: str = ""  # OpenRouter API key（环境变量 OPENROUTER_API_KEY）
    openclaw_api_key: str = ""  # OpenClaw API key（环境变量 OPENCLAW_API_KEY）
    openclaw_url: str = "http://localhost:18789"  # OpenClaw Gateway 地址
    observer_provider_priority: list[str] = field(
        default_factory=lambda: ["claude", "gemini", "openrouter", "openclaw", "ollama"],
    )

    # ── Observer Worker 配置（#247）──
    observer_worker_enabled: bool = True  # 是否启用持久 observer worker 模式
    observer_worker_port: int = 6889  # Observer worker HTTP 端口

    def __post_init__(self):
        if not self.db_path:
            self.db_path = str(INDEX1_HOME / "knowledge.db")
        if not self.cognitive_db_path:
            self.cognitive_db_path = str(INDEX1_HOME / "cognitive.db")
        valid_apis = ("ollama", "openai_compat")
        if self.embed_api not in valid_apis:
            raise ValueError(
                f"embed_api 必须是 {valid_apis} 之一，当前值: '{self.embed_api}'"
            )
        valid_backends = ("onnx", "ollama")
        if self.embed_backend not in valid_backends:
            raise ValueError(
                f"embed_backend 必须是 {valid_backends} 之一，当前值: '{self.embed_backend}'"
            )

    @property
    def db_path_resolved(self) -> Path:
        return Path(self.db_path).expanduser()

    @property
    def cognitive_db_path_resolved(self) -> Path:
        return Path(self.cognitive_db_path).expanduser()

    @property
    def effective_embedding_dim(self) -> int:
        """返回当前 embedding 后端的实际维度"""
        if self.embed_backend == "onnx":
            return self.onnx_dim
        return self.embedding_dim


# ── Observer 默认模型注册表（集中管理，改一处全局生效）──
# 使用非版本化别名 → API 自动跟随最新版本
# 更新 Sonnet 大版本时只改这里
OBSERVER_MODELS: dict[str, str] = {
    "claude_cli": "claude-sonnet-4-5",          # Claude CLI（Max 订阅）
    "claude_api": "claude-sonnet-4-5",          # Anthropic Messages API（非版本化 → 自动最新）
    "openrouter": "anthropic/claude-sonnet-4",  # OpenRouter 聚合 API
    "gemini": "gemini-2.0-flash",               # Google Gemini（自有模型）
    "openclaw": "default",                      # OpenClaw vLLM Gateway
    "ollama": "llama3.2:3b",                    # Ollama 本地模型
}


def _load_yaml(path: Path) -> dict:
    """加载 YAML 文件，不存在或解析失败返回空 dict"""
    if not path.is_file():
        return {}
    try:
        with open(path, encoding="utf-8") as f:
            data = yaml.safe_load(f)
            return data if isinstance(data, dict) else {}
    except (OSError, yaml.YAMLError):
        return {}


def _merge_into_config(config: Config, overrides: dict) -> None:
    """将 dict 中的有效键合并到 Config 实例"""
    valid_fields = {f.name for f in fields(Config)}
    for key, value in overrides.items():
        if key in valid_fields and value is not None:
            setattr(config, key, value)


def load_config(project_path: Path | None = None) -> Config:
    """加载配置，三层合并：默认值 → 全局 → 项目级

    Args:
        project_path: 项目根目录，用于查找 .index1.yaml。
                      None 时使用当前工作目录。
    """
    # 确保配置目录存在
    try:
        INDEX1_HOME.mkdir(parents=True, exist_ok=True)
    except OSError:
        pass

    # Layer 1: 默认值
    config = Config()

    # Layer 2: 全局配置 ~/.claude-index1/config.yaml
    global_overrides = _load_yaml(GLOBAL_CONFIG_FILE)
    _merge_into_config(config, global_overrides)

    # Layer 3: 项目配置 .index1.yaml
    if project_path is None:
        project_path = Path.cwd()
    project_config = project_path / PROJECT_CONFIG_FILE
    project_overrides = _load_yaml(project_config)
    _merge_into_config(config, project_overrides)

    return config


def _get_memory_gb() -> float:
    """获取系统物理内存 (GB)，失败返回 16.0 作为默认值"""
    try:
        import os
        if hasattr(os, "sysconf"):
            # macOS + Linux
            page_size = os.sysconf("SC_PAGE_SIZE")
            phys_pages = os.sysconf("SC_PHYS_PAGES")
            if page_size > 0 and phys_pages > 0:
                return (page_size * phys_pages) / (1024 ** 3)
        else:
            # Windows
            import ctypes
            kernel32 = ctypes.windll.kernel32
            mem_kb = ctypes.c_ulonglong()
            ret = kernel32.GetPhysicallyInstalledSystemMemory(ctypes.byref(mem_kb))
            if ret and mem_kb.value > 0:
                return (mem_kb.value * 1024) / (1024 ** 3)
    except (AttributeError, ValueError, OSError):
        pass
    return 16.0


def auto_select_model() -> str:
    """根据系统内存自动选择 embedding 模型档位

    自动选择（按内存）:
    - < 12 GB: lightweight (all-minilm, 384 维)
    - 12-32 GB: standard (nomic-embed-text, 768 维)
    - >= 32 GB: high_precision (nomic-embed-text-v2-moe, 768 维)

    用户手动选择（按语言需求）:
    - chinese: bge-m3 (1024 维) — 中文最优，100+ 语言
    - multilingual: paraphrase-multilingual (768 维) — 50+ 语言通用

    Returns:
        MODEL_PROFILES 中的 key
    """
    mem_gb = _get_memory_gb()

    if mem_gb < 12:
        return "lightweight"
    elif mem_gb >= 32:
        return "high_precision"
    return "standard"


def save_config(config: Config, path: Path | None = None) -> None:
    """保存配置到 YAML 文件

    Args:
        path: 保存路径。None 时保存到全局配置文件。
    """
    if path is None:
        path = GLOBAL_CONFIG_FILE
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
    except OSError:
        pass

    data = {}
    default = Config()
    for f in fields(Config):
        value = getattr(config, f.name)
        default_value = getattr(default, f.name)
        # 只保存非默认值
        if value != default_value:
            data[f.name] = value

    try:
        with open(path, "w", encoding="utf-8") as f:
            yaml.dump(data, f, default_flow_style=False, allow_unicode=True)
    except OSError:
        pass
