#!/usr/bin/env python3
"""Migrate legacy inline LLM credentials into centralized model configs."""

import argparse
import hashlib
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from sqlmodel import Session, select

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "backend"))

from agenteval.services.model_configs import SecretCipher, SecretKeyError  # noqa: E402
from agenteval.storage.db import (  # noqa: E402
    ModelConfigDB,
    ScenarioDB,
    ScenarioModelBindingDB,
    engine,
)


@dataclass(frozen=True)
class LegacyConfig:
    capability: str
    endpoint_url: str
    model_name: str | None
    api_key: str | None

    @property
    def fingerprint(self) -> str:
        source = "\0".join((self.capability, self.endpoint_url, self.model_name or "", self.api_key or ""))
        return hashlib.sha256(source.encode("utf-8")).hexdigest()[:12]


@dataclass
class ScenarioMigration:
    scenario: ScenarioDB
    configs: dict[str, LegacyConfig]


def _legacy_from_params(capability: str, params: dict[str, Any]) -> LegacyConfig | None:
    endpoint = str(params.get("api_url") or "").strip()
    if not endpoint:
        return None
    model = str(params.get("model") or "").strip() or None
    api_key = str(params.get("api_key") or "").strip() or None
    return LegacyConfig(capability, endpoint, model, api_key)


def _scan_scenario(scenario: ScenarioDB) -> ScenarioMigration | None:
    by_purpose: dict[str, list[LegacyConfig]] = {}
    if scenario.get_llm_config():
        generator = _legacy_from_params("chat", scenario.get_llm_config() or {})
        if generator:
            by_purpose.setdefault("generator", []).append(generator)

    for case in scenario.get_cases():
        for rule in case.get("eval_rules", []):
            rule_type = rule.get("type")
            params = rule.get("params") or {}
            if rule_type == "llm_score":
                legacy = _legacy_from_params("chat", params)
                if legacy:
                    by_purpose.setdefault("judge", []).append(legacy)
            elif rule_type == "semantic_similarity":
                legacy = _legacy_from_params("embedding", params)
                if legacy:
                    by_purpose.setdefault("embedding", []).append(legacy)
            elif rule_type == "safety" and params.get("use_moderation_api"):
                legacy = _legacy_from_params("moderation", params)
                if legacy:
                    by_purpose.setdefault("moderation", []).append(legacy)

    configs: dict[str, LegacyConfig] = {}
    for purpose, candidates in by_purpose.items():
        unique = {item.fingerprint: item for item in candidates}
        if len(unique) > 1:
            raise ValueError(f"场景 {scenario.id} 的 {purpose} 用途存在多个不同旧配置，请先手工合并")
        configs[purpose] = next(iter(unique.values()))
    return ScenarioMigration(scenario, configs) if configs else None


def _strip_legacy_fields(scenario: ScenarioDB, migrated_purposes: set[str]) -> None:
    if "generator" in migrated_purposes:
        scenario.set_llm_config(None)
    rule_purposes = {
        "llm_score": "judge",
        "semantic_similarity": "embedding",
        "safety": "moderation",
    }
    cases = scenario.get_cases()
    for case in cases:
        for rule in case.get("eval_rules", []):
            if rule_purposes.get(rule.get("type")) not in migrated_purposes:
                continue
            params = rule.get("params") or {}
            for field in ("api_url", "api_key", "model"):
                params.pop(field, None)
            rule["params"] = params
    scenario.set_cases(cases)


def _config_name(config: LegacyConfig) -> str:
    model = (config.model_name or "endpoint").replace("/", "-")[:40]
    return f"迁移-{config.capability}-{model}-{config.fingerprint[:8]}"


def migrate(session: Session, apply: bool) -> dict[str, int]:
    existing_bindings = {
        (binding.scenario_id, binding.purpose): binding
        for binding in session.exec(select(ScenarioModelBindingDB)).all()
    }
    migrations = [
        migration
        for scenario in session.exec(select(ScenarioDB)).all()
        if (migration := _scan_scenario(scenario)) is not None
    ]
    pending = [
        (migration, purpose, config)
        for migration in migrations
        for purpose, config in migration.configs.items()
        if (migration.scenario.id or "", purpose) not in existing_bindings
    ]
    summary = {
        "scenarios": len(migrations),
        "bindings": len(pending),
        "configs": len({config.fingerprint for _, _, config in pending}),
    }
    if not apply:
        return summary

    cipher = SecretCipher()
    configs_by_fingerprint: dict[str, ModelConfigDB] = {}
    existing_by_name = {
        config.name: config for config in session.exec(select(ModelConfigDB)).all()
    }
    try:
        for _, _, legacy in pending:
            if legacy.fingerprint in configs_by_fingerprint:
                continue
            name = _config_name(legacy)
            config = existing_by_name.get(name)
            if config is None:
                config = ModelConfigDB(
                    name=name,
                    provider="openai_compatible",
                    capability=legacy.capability,
                    endpoint_url=legacy.endpoint_url,
                    model_name=legacy.model_name,
                    api_key_encrypted=cipher.encrypt(legacy.api_key),
                    enabled=True,
                    is_default=False,
                    description="由 v0.3.0 旧配置迁移工具创建",
                )
                session.add(config)
                session.flush()
            configs_by_fingerprint[legacy.fingerprint] = config

        for migration in migrations:
            migrated_purposes: set[str] = set()
            scenario_id = migration.scenario.id or ""
            for purpose, legacy in migration.configs.items():
                binding = existing_bindings.get((scenario_id, purpose))
                if binding is None:
                    config = configs_by_fingerprint[legacy.fingerprint]
                    session.add(
                        ScenarioModelBindingDB(
                            scenario_id=scenario_id,
                            purpose=purpose,
                            model_config_id=config.id or "",
                        )
                    )
                migrated_purposes.add(purpose)
            _strip_legacy_fields(migration.scenario, migrated_purposes)
            session.add(migration.scenario)
        session.commit()
    except Exception:
        session.rollback()
        raise
    return summary


def main() -> int:
    parser = argparse.ArgumentParser(description="迁移场景中的旧模型配置；默认仅预览")
    parser.add_argument("--apply", action="store_true", help="执行迁移写入；不传时仅预览")
    args = parser.parse_args()
    try:
        with Session(engine) as session:
            summary = migrate(session, apply=args.apply)
    except (ValueError, SecretKeyError) as exc:
        print(f"迁移终止: {exc}", file=sys.stderr)
        return 1
    mode = "已完成" if args.apply else "预览"
    print(
        f"{mode}: 场景 {summary['scenarios']} 个，新增绑定 {summary['bindings']} 个，"
        f"模型配置 {summary['configs']} 个"
    )
    if not args.apply:
        print("未写入数据库；确认后使用 --apply 执行")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
