"""把存量 Factory MES definition 一次性转换为通用 OpenAPI definition。

脚本默认只预检。数据库地址只从显式的 ``ZCBOT_MIGRATION_DB_URL`` 读取，
不会加载项目 ``.env``，也不会回退到 ``ZCBOT_DB_URL``。
"""

from __future__ import annotations

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

from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from core.external_systems.service import _normalized_config  # noqa: E402
from core.storage.models import (  # noqa: E402
    ExternalSystem,
    ExternalSystemDefinition,
)


FACTORY_QUERY_GUIDANCE = (
    "产量、良率、缺陷、库存、绩效、趋势和按日/月汇总等统计聚合查询，"
    "统一先调用 BI dataset list，再执行匹配的数据集。日志和业务明细列表用于"
    "用户明确要求查看逐条记录、编号或追溯过程的场景。未匹配到 dataset 时，"
    "先限定范围或向用户确认明细查询需求。"
)


def migrated_config(raw: dict[str, Any] | None) -> dict[str, Any]:
    """物化旧 preset，并收敛为 query 模式的通用 OpenAPI 配置。"""
    source = dict(raw or {})
    policies = {
        str(key).strip(): str(value).strip().lower()
        for key, value in (source.get("operation_policies") or {}).items()
        if str(key).strip()
    }
    for operation_id in source.get("allowed_post_operations") or []:
        if str(operation_id).strip():
            policies.setdefault(str(operation_id).strip(), "read")
    policies.setdefault("bi_dataset_exec", "read")
    source.update(
        {
            "auth_type": source.get("auth_type") or "password_jwt",
            "login_path": source.get("login_path") or "/api/auth/token/",
            "username_field": source.get("username_field") or "username",
            "password_field": source.get("password_field") or "password",
            "token_field": source.get("token_field") or "access",
            "auth_header_name": source.get("auth_header_name") or "Authorization",
            "auth_header_template": source.get("auth_header_template")
            or "Bearer {token}",
            "operation_mode": "query",
            "operation_policies": policies,
            "query_guidance": source.get("query_guidance")
            or FACTORY_QUERY_GUIDANCE,
            "recommended_operation_ids": source.get("recommended_operation_ids")
            or ["bi_dataset_list", "bi_dataset_exec"],
        }
    )
    source.pop("allowed_post_operations", None)
    return _normalized_config("generic_openapi", source)


def _conflicting_definition(
    session: Session, definition: ExternalSystemDefinition
) -> ExternalSystemDefinition | None:
    owner_match = (
        ExternalSystemDefinition.owner_type == "platform"
        if definition.owner_type == "platform"
        else ExternalSystemDefinition.owner_user_id == definition.owner_user_id
    )
    return session.execute(
        select(ExternalSystemDefinition).where(
            ExternalSystemDefinition.provider == "generic_openapi",
            ExternalSystemDefinition.name == definition.name,
            owner_match,
        )
    ).scalar_one_or_none()


def migrate(session: Session, *, apply: bool) -> tuple[int, int]:
    statement = (
        select(ExternalSystemDefinition)
        .where(ExternalSystemDefinition.provider == "factory_mes")
        .order_by(ExternalSystemDefinition.name)
    )
    if apply:
        statement = statement.with_for_update()
    definitions = (
        session.execute(statement)
        .scalars()
        .all()
    )
    connection_count = 0
    prepared: list[tuple[ExternalSystemDefinition, dict[str, Any], int]] = []
    for definition in definitions:
        conflict = _conflicting_definition(session, definition)
        if conflict is not None:
            raise RuntimeError(
                f"definition name conflict: {definition.name} "
                f"({definition.definition_id} vs {conflict.definition_id})"
            )
        config = migrated_config(definition.config)
        count = len(
            session.execute(
                select(ExternalSystem.external_system_id).where(
                    ExternalSystem.definition_id == definition.definition_id
                )
            ).all()
        )
        connection_count += count
        prepared.append((definition, config, count))
        print(
            f"[INFO] {definition.definition_id} name={definition.name!r} "
            f"connections={count}"
        )

    print(f"[INFO] factory_mes definitions: {len(prepared)}")
    print(f"[INFO] affected connections: {connection_count}")
    if not apply:
        return len(prepared), connection_count

    for definition, config, _ in prepared:
        definition.provider = "generic_openapi"
        definition.config = config
        definition.revision += 1
        active_connections = (
            session.execute(
                select(ExternalSystem).where(
                    ExternalSystem.definition_id == definition.definition_id,
                    ExternalSystem.status == "active",
                )
            )
            .scalars()
            .all()
        )
        for connection in active_connections:
            connection.verified_revision = definition.revision
    session.flush()
    remaining = session.execute(
        select(ExternalSystemDefinition.definition_id).where(
            ExternalSystemDefinition.provider == "factory_mes"
        )
    ).first()
    if remaining is not None:
        raise RuntimeError("factory_mes definitions remain after migration")
    print("[OK] remaining factory_mes definitions: 0")
    return len(prepared), connection_count


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--apply",
        action="store_true",
        help="执行写入；省略时只做预检并回滚事务",
    )
    args = parser.parse_args()
    database_url = os.environ.get("ZCBOT_MIGRATION_DB_URL", "").strip()
    if not database_url:
        print("[ERR] ZCBOT_MIGRATION_DB_URL is required", file=sys.stderr)
        return 2
    engine = create_engine(database_url, pool_pre_ping=True, future=True)
    try:
        with Session(engine, future=True) as session:
            try:
                definitions, connections = migrate(session, apply=args.apply)
                if args.apply:
                    session.commit()
                else:
                    session.rollback()
            except Exception:
                session.rollback()
                raise
    finally:
        engine.dispose()
    action = "migrated" if args.apply else "validated"
    print(f"[OK] {action} definitions: {definitions}")
    print(f"[OK] affected connections: {connections}")
    return 0


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