"""规则服务实现。"""

from __future__ import annotations

import hashlib
import logging
import time
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
from fastapi_common.fastapi_common_web.domain.responses import StatusCodeEnum
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException
from fastapi_common.fastapi_common_storage.oss_path_utils import OssPathUtils
from sqlalchemy import text

from fastapi_modules.fastapi_leaudit.domian.vo.ruleVo import (
    RuleBindingVO,
    RuleContentVO,
    RuleSetVO,
    RuleValidationVO,
    RuleVersionVO,
)
from fastapi_modules.fastapi_leaudit.leaudit_bridge.ruleValidator import RuleValidator
from fastapi_modules.fastapi_leaudit.services import IOssService, IRuleService
from fastapi_modules.fastapi_leaudit.services.impl.ossServiceImpl import OssServiceImpl
from fastapi_modules.fastapi_leaudit.services.impl.ruleGroupSupport import sync_doc_type_bindings_from_group


class RuleServiceImpl(IRuleService):
    """规则服务实现。"""

    _GLOBAL_LIST_SETS_CACHE: tuple[float, list[RuleSetVO]] | None = None
    _GLOBAL_RULE_COUNT_CACHE: dict[int, tuple[float, int]] = {}

    def __init__(
        self,
        OssService: IOssService | None = None,
        Validator: RuleValidator | None = None,
    ) -> None:
        self.OssService = OssService or OssServiceImpl()
        self.Validator = Validator or RuleValidator()
        self._list_sets_cache = self.__class__._GLOBAL_LIST_SETS_CACHE
        self._rule_count_cache = self.__class__._GLOBAL_RULE_COUNT_CACHE

    @classmethod
    def _set_list_sets_cache(cls, value: tuple[float, list[RuleSetVO]] | None) -> None:
        cls._GLOBAL_LIST_SETS_CACHE = value

    def InvalidateCaches(self, version_ids: list[int] | None = None) -> None:
        """清理规则集及规则数量缓存。"""
        self.__class__._set_list_sets_cache(None)
        if version_ids is None:
            self._rule_count_cache.clear()
            return
        for version_id in {int(item) for item in version_ids if item is not None}:
            self._rule_count_cache.pop(version_id, None)

    async def _resolve_unique_child_group_id(self, Session, DocTypeId: int) -> int | None:
        """仅当文档类型唯一对应一个二级分组时，返回该分组ID。"""
        row = (
            await Session.execute(
                text(
                    """
                    SELECT CASE WHEN COUNT(*) = 1 THEN MIN(id) END AS group_id
                    FROM leaudit_evaluation_point_groups
                    WHERE document_type_id = :doc_type_id
                      AND deleted_at IS NULL
                      AND COALESCE(pid, 0) <> 0
                    """
                ),
                {"doc_type_id": DocTypeId},
            )
        ).mappings().first()
        return int(row["group_id"]) if row and row.get("group_id") is not None else None

    async def _count_child_groups(self, Session, DocTypeId: int) -> int:
        """统计文档类型下已启用的二级分组数量，用于明确报错。"""
        row = (
            await Session.execute(
                text(
                    """
                    SELECT COUNT(*) AS total
                    FROM leaudit_evaluation_point_groups
                    WHERE document_type_id = :doc_type_id
                      AND deleted_at IS NULL
                      AND COALESCE(pid, 0) <> 0
                    """
                ),
                {"doc_type_id": DocTypeId},
            )
        ).mappings().first()
        return int(row["total"] or 0) if row else 0

    async def ListSets(self) -> list[RuleSetVO]:
        """列出所有规则集。"""
        now = time.monotonic()
        cached = self.__class__._GLOBAL_LIST_SETS_CACHE
        if cached and now - cached[0] <= 30:
            return cached[1]

        async with GetAsyncSession() as Session:
            Result = await Session.execute(
                text(
                    """
                    SELECT
                        rs.id,
                        rs.rule_type,
                        rs.rule_name,
                        rs.domain_type,
                        rs.current_version_id,
                        current_rv.id AS usable_current_version_id,
                        fallback_rv.id AS fallback_version_id,
                        CASE
                            WHEN current_rv.id IS NOT NULL OR fallback_rv.id IS NOT NULL THEN TRUE
                            ELSE FALSE
                        END AS has_usable_version,
                        rs.status
                    FROM leaudit_rule_sets rs
                    LEFT JOIN leaudit_rule_versions current_rv
                      ON current_rv.id = rs.current_version_id
                     AND current_rv.status = 'published'
                    LEFT JOIN LATERAL (
                        SELECT rv.id
                        FROM leaudit_rule_versions rv
                        WHERE rv.rule_set_id = rs.id
                          AND rv.status = 'published'
                          AND (rs.current_version_id IS NULL OR rv.id <> rs.current_version_id)
                        ORDER BY rv.version_seq DESC, rv.id DESC
                        LIMIT 1
                    ) fallback_rv ON TRUE
                    WHERE rs.deleted_at IS NULL
                    ORDER BY rs.id DESC
                    """
                )
            )
            rows = Result.mappings().all()

        usable_counts: dict[int, int] = {}
        for row in rows:
            usable_version_id = row["usable_current_version_id"] or row["fallback_version_id"]
            if usable_version_id is not None and int(usable_version_id) not in usable_counts:
                usable_counts[int(usable_version_id)] = await self._GetRuleCountByVersionId(int(usable_version_id))

        items = [
            RuleSetVO(
                id=int(Row["id"]),
                ruleType=Row["rule_type"],
                ruleName=Row["rule_name"],
                domainType=Row["domain_type"],
                currentVersionId=Row["current_version_id"],
                fallbackVersionId=Row["fallback_version_id"],
                hasUsableVersion=bool(Row["has_usable_version"]),
                usableRuleCount=usable_counts.get(int(Row["usable_current_version_id"] or Row["fallback_version_id"]), 0)
                if (Row["usable_current_version_id"] or Row["fallback_version_id"]) is not None
                else 0,
                status=Row["status"],
            )
            for Row in rows
        ]
        cache_value = (time.monotonic(), items)
        self.__class__._set_list_sets_cache(cache_value)
        self._list_sets_cache = cache_value
        return items

    async def _GetRuleCountByVersionId(self, VersionId: int) -> int:
        """读取指定可用规则版本的规则数。"""
        cached = self._rule_count_cache.get(VersionId)
        now = time.monotonic()
        if cached and now - cached[0] <= 120:
            return cached[1]

        async with GetAsyncSession() as Session:
            Result = await Session.execute(
                text(
                    """
                    SELECT oss_url
                    FROM leaudit_rule_versions
                    WHERE id = :version_id
                    LIMIT 1
                    """
                ),
                {"version_id": VersionId},
            )
            Row = Result.mappings().first()

        if not Row or not Row["oss_url"]:
            return 0

        try:
            yaml_text = (await self.OssService.DownloadBytes(Row["oss_url"])).decode("utf-8")
            validation = self.Validator.ValidateYaml(yaml_text)
            count = int(validation.ruleCount or 0)
            self._rule_count_cache[VersionId] = (time.monotonic(), count)
            return count
        except Exception:
            return 0

    async def GetVersions(self, RuleType: str) -> list[RuleVersionVO]:
        """获取规则集的所有版本。"""
        async with GetAsyncSession() as Session:
            Result = await Session.execute(
                text(
                    """
                    SELECT
                        rv.id,
                        rv.rule_set_id,
                        rv.version_no,
                        rv.status,
                        rv.oss_url,
                        rv.change_note,
                        rv.published_at
                    FROM leaudit_rule_versions rv
                    JOIN leaudit_rule_sets rs ON rs.id = rv.rule_set_id
                    WHERE rs.rule_type = :rule_type
                      AND rs.deleted_at IS NULL
                    ORDER BY rv.version_seq DESC, rv.id DESC
                    """
                ),
                {"rule_type": RuleType},
            )
            return [self._BuildRuleVersionVo(Row) for Row in Result.mappings().all()]

    async def GetContent(self, VersionId: int) -> RuleContentVO:
        """获取指定版本的规则正文。"""
        async with GetAsyncSession() as Session:
            Result = await Session.execute(
                text(
                    """
                    SELECT
                        rv.id,
                        rv.rule_set_id,
                        rv.version_no,
                        rv.oss_url,
                        rs.rule_type
                    FROM leaudit_rule_versions rv
                    JOIN leaudit_rule_sets rs ON rs.id = rv.rule_set_id
                    WHERE rv.id = :version_id
                    LIMIT 1
                    """
                ),
                {"version_id": VersionId},
            )
            Row = Result.mappings().first()
            if not Row:
                raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则版本不存在")

        YamlText = (await self.OssService.DownloadBytes(Row["oss_url"])).decode("utf-8")
        return RuleContentVO(
            id=int(Row["id"]),
            ruleSetId=int(Row["rule_set_id"]),
            ruleType=Row["rule_type"],
            versionNo=Row["version_no"],
            yamlText=YamlText,
            ossUrl=Row["oss_url"],
        )

    async def Validate(self, RuleType: str, YamlText: str) -> RuleValidationVO:
        """校验规则 YAML。"""
        Validation = self.Validator.ValidateYaml(YamlText)
        if Validation.ruleType and Validation.ruleType != RuleType:
            Validation = RuleValidationVO(
                valid=False,
                ruleType=Validation.ruleType,
                ruleName=Validation.ruleName,
                versionNo=Validation.versionNo,
                ruleCount=Validation.ruleCount,
                extractCount=Validation.extractCount,
                errors=[f"路径规则类型 {RuleType} 与 YAML metadata.type_id {Validation.ruleType} 不一致"],
            )
            return Validation

        return RuleValidationVO(
            valid=Validation.valid,
            ruleType=Validation.ruleType,
            ruleName=Validation.ruleName,
            versionNo=Validation.versionNo,
            ruleCount=Validation.ruleCount,
            extractCount=Validation.extractCount,
            errors=Validation.errors or [],
        )

    async def CreateVersion(
        self,
        RuleType: str,
        YamlText: str,
        ChangeNote: str | None = None,
        EditorUserId: int | None = None,
    ) -> RuleVersionVO:
        """创建规则版本。"""
        Validation = await self.Validate(RuleType, YamlText)
        if not Validation.valid:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "; ".join(Validation.errors))

        RulesFile = self.Validator.ParseValidated(YamlText)
        FileSha256 = hashlib.sha256(YamlText.encode("utf-8")).hexdigest()
        FileSize = len(YamlText.encode("utf-8"))
        DomainType = self._ResolveDomainType(RuleType)

        async with GetAsyncSession() as Session:
            RuleSet = await self._GetRuleSetByType(Session, RuleType)
            if not RuleSet:
                CreateResult = await Session.execute(
                    text(
                        """
                        INSERT INTO leaudit_rule_sets (
                            rule_type,
                            rule_name,
                            domain_type,
                            description,
                            status,
                            is_builtin,
                            owner_user_id
                        ) VALUES (
                            :rule_type,
                            :rule_name,
                            :domain_type,
                            :description,
                            'draft',
                            false,
                            :owner_user_id
                        )
                        RETURNING id, rule_type, rule_name, domain_type, current_version_id, status
                        """
                    ),
                    {
                        "rule_type": RuleType,
                        "rule_name": RulesFile.metadata.name,
                        "domain_type": DomainType,
                        "description": RulesFile.metadata.description,
                        "owner_user_id": EditorUserId,
                    },
                )
                RuleSet = CreateResult.mappings().first()
            else:
                await Session.execute(
                    text(
                        """
                        UPDATE leaudit_rule_sets
                        SET
                            rule_name = :rule_name,
                            domain_type = :domain_type,
                            description = :description,
                            updated_at = now()
                        WHERE id = :rule_set_id
                        """
                    ),
                    {
                        "rule_name": RulesFile.metadata.name,
                        "domain_type": DomainType,
                        "description": RulesFile.metadata.description,
                        "rule_set_id": RuleSet["id"],
                    },
                )

            SeqResult = await Session.execute(
                text(
                    """
                    SELECT COALESCE(MAX(version_seq), 0) AS max_seq
                    FROM leaudit_rule_versions
                    WHERE rule_set_id = :rule_set_id
                    """
                ),
                {"rule_set_id": RuleSet["id"]},
            )
            NextSeq = int(SeqResult.mappings().first()["max_seq"]) + 1
            VersionNo = RulesFile.metadata.version or f"v{NextSeq}"
            ExistingVersion = await Session.execute(
                text(
                    """
                    SELECT id
                    FROM leaudit_rule_versions
                    WHERE rule_set_id = :rule_set_id
                      AND version_no = :version_no
                    LIMIT 1
                    """
                ),
                {
                    "rule_set_id": RuleSet["id"],
                    "version_no": VersionNo,
                },
            )
            if ExistingVersion.mappings().first():
                raise LeauditException(StatusCodeEnum.HTTP_409_CONFLICT, f"版本号 {VersionNo} 已存在")

            ObjectKey = OssPathUtils.BuildRuleYamlKey(RuleType, VersionNo)
            OssUrl = await self.OssService.UploadText(
                ObjectKey=ObjectKey,
                Content=YamlText,
                ContentType="application/x-yaml; charset=utf-8",
            )

            VersionResult = await Session.execute(
                text(
                    """
                    INSERT INTO leaudit_rule_versions (
                        rule_set_id,
                        version_no,
                        version_seq,
                        status,
                        source_type,
                        dsl_format,
                        oss_url,
                        file_sha256,
                        file_size,
                        metadata_type_id,
                        metadata_name,
                        metadata_version,
                        change_note,
                        editor_user_id
                    ) VALUES (
                        :rule_set_id,
                        :version_no,
                        :version_seq,
                        'draft',
                        'oss_yaml',
                        'yaml',
                        :oss_url,
                        :file_sha256,
                        :file_size,
                        :metadata_type_id,
                        :metadata_name,
                        :metadata_version,
                        :change_note,
                        :editor_user_id
                    )
                    RETURNING id, rule_set_id, version_no, status, oss_url, change_note, published_at
                    """
                ),
                {
                    "rule_set_id": RuleSet["id"],
                    "version_no": VersionNo,
                    "version_seq": NextSeq,
                    "oss_url": OssUrl,
                    "file_sha256": FileSha256,
                    "file_size": FileSize,
                    "metadata_type_id": RulesFile.metadata.type_id,
                    "metadata_name": RulesFile.metadata.name,
                    "metadata_version": RulesFile.metadata.version,
                    "change_note": ChangeNote,
                    "editor_user_id": EditorUserId,
                },
            )
            await Session.commit()
            return self._BuildRuleVersionVo(VersionResult.mappings().first())

    async def Publish(
        self,
        RuleType: str,
        VersionId: int,
        OperatorUserId: int | None = None,
    ) -> RuleVersionVO:
        """发布指定版本。"""
        return await self._SwitchVersion(
            RuleType=RuleType,
            VersionId=VersionId,
            TargetStatus="published",
            OperatorUserId=OperatorUserId,
        )

    async def Rollback(
        self,
        RuleType: str,
        VersionId: int,
        OperatorUserId: int | None = None,
    ) -> RuleVersionVO:
        """回滚到指定历史版本。"""
        return await self._SwitchVersion(
            RuleType=RuleType,
            VersionId=VersionId,
            TargetStatus="published",
            OperatorUserId=OperatorUserId,
            Rollback=True,
        )

    async def ListBindings(self, RuleType: str | None = None, Region: str | None = None) -> list[RuleBindingVO]:
        """列出规则类型绑定，只读取新分组绑定。"""
        async with GetAsyncSession() as Session:
            params: dict[str, object] = {}
            filters = ["rs.deleted_at IS NULL", "dt.deleted_at IS NULL", "child.deleted_at IS NULL", "rgb.deleted_at IS NULL"]
            if RuleType:
                filters.append("rs.rule_type = :rule_type")
                params["rule_type"] = RuleType
            where_clause = " AND ".join(filters)

            result = await Session.execute(
                text(
                    f"""
                    SELECT
                        rgb.id,
                        dt.id AS doc_type_id,
                        dt.code AS doc_type_code,
                        rgb.rule_set_id,
                        'explicit' AS binding_mode,
                        rgb.priority,
                        rgb.is_active,
                        rgb.note,
                        rs.rule_type,
                        rs.rule_name
                    FROM leaudit_rule_group_bindings rgb
                    JOIN leaudit_evaluation_point_groups child ON child.id = rgb.group_id
                    JOIN leaudit_document_types dt ON dt.id = child.document_type_id
                    JOIN leaudit_rule_sets rs ON rs.id = rgb.rule_set_id
                    WHERE {where_clause}
                      AND COALESCE(child.pid, 0) <> 0
                    ORDER BY dt.id ASC, COALESCE(child.sort_order, 0) ASC, rgb.priority DESC, rgb.id DESC
                    """
                ),
                params,
            )
            rows = result.mappings().all()
            bindings: list[RuleBindingVO] = []
            seen_doc_type_rule_pairs: set[tuple[int, int]] = set()
            for row in rows:
                pair = (int(row["doc_type_id"]), int(row["rule_set_id"]))
                if pair in seen_doc_type_rule_pairs:
                    continue
                seen_doc_type_rule_pairs.add(pair)
                bindings.append(
                    RuleBindingVO(
                        id=int(row["id"]),
                        docTypeId=int(row["doc_type_id"]),
                        docTypeCode=row["doc_type_code"],
                        ruleSetId=int(row["rule_set_id"]),
                        ruleType=row["rule_type"],
                        ruleName=row["rule_name"],
                        bindingMode=row["binding_mode"],
                        priority=int(row["priority"]),
                        isActive=bool(row["is_active"]),
                        note=row["note"],
                    )
                )
            return bindings

    async def CreateBinding(
        self,
        DocTypeId: int,
        RuleSetId: int,
        Region: str = "default",
        BindingMode: str = "explicit",
        Priority: int = 0,
        DocTypeCode: str | None = None,
        Note: str | None = None,
    ) -> RuleBindingVO:
        """创建规则类型绑定；若文档类型唯一对应一个二级分组，则优先写入新分组绑定。"""
        async with GetAsyncSession() as Session:
            RuleSet = await Session.execute(
                text("SELECT id, rule_type, rule_name FROM leaudit_rule_sets WHERE id = :rid AND deleted_at IS NULL LIMIT 1"),
                {"rid": RuleSetId},
            )
            RsRow = RuleSet.mappings().first()
            if not RsRow:
                raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则集不存在")

            GroupId = await self._resolve_unique_child_group_id(Session, DocTypeId)
            if GroupId is not None:
                ExistingGroupBinding = await Session.execute(
                    text(
                        """
                        SELECT id
                        FROM leaudit_rule_group_bindings
                        WHERE group_id = :group_id
                          AND rule_set_id = :rule_set_id
                          AND deleted_at IS NULL
                        LIMIT 1
                        """
                    ),
                    {"group_id": GroupId, "rule_set_id": RuleSetId},
                )
                if ExistingGroupBinding.mappings().first():
                    raise LeauditException(StatusCodeEnum.HTTP_409_CONFLICT, "该文档类型对应子组已绑定此规则集")

                Result = await Session.execute(
                    text(
                        """
                        INSERT INTO leaudit_rule_group_bindings (
                            group_id,
                            rule_set_id,
                            priority,
                            is_active,
                            note,
                            created_at,
                            updated_at
                        ) VALUES (
                            :group_id,
                            :rule_set_id,
                            :priority,
                            true,
                            :note,
                            NOW(),
                            NOW()
                        )
                        RETURNING id, rule_set_id, priority, is_active, note
                        """
                    ),
                    {
                        "group_id": GroupId,
                        "rule_set_id": RuleSetId,
                        "priority": Priority,
                        "note": Note,
                    },
                )
                await sync_doc_type_bindings_from_group(Session, GroupId)
                await Session.commit()
                Row = Result.mappings().first()
                return RuleBindingVO(
                    id=int(Row["id"]),
                    docTypeId=DocTypeId,
                    docTypeCode=DocTypeCode,
                    ruleSetId=int(Row["rule_set_id"]),
                    ruleType=RsRow["rule_type"],
                    ruleName=RsRow["rule_name"],
                    bindingMode="explicit",
                    priority=int(Row["priority"]),
                    isActive=bool(Row["is_active"]),
                    note=Row["note"],
                )

            child_group_count = await self._count_child_groups(Session, DocTypeId)
            if child_group_count == 0:
                raise LeauditException(
                    StatusCodeEnum.HTTP_409_CONFLICT,
                    "当前文档类型尚未创建运行子类型，无法绑定规则集；请先在评查点分组管理中补齐二级分组",
                )
            raise LeauditException(
                StatusCodeEnum.HTTP_409_CONFLICT,
                "当前文档类型存在多个运行子类型，不能再直接按文档类型绑定规则集；请改为在对应二级分组下绑定规则集",
            )

    async def UpdateBinding(
        self,
        BindingId: int,
        IsActive: bool | None = None,
        Priority: int | None = None,
        BindingMode: str | None = None,
        Note: str | None = None,
    ) -> RuleBindingVO:
        """更新规则类型绑定；若绑定ID来自新分组绑定，则优先更新新表。"""
        async with GetAsyncSession() as Session:
            GroupBinding = await Session.execute(
                text(
                    """
                    SELECT
                        rgb.id,
                        rgb.group_id,
                        child.document_type_id AS doc_type_id,
                        dt.code AS doc_type_code,
                        rgb.rule_set_id,
                        rgb.priority,
                        rgb.is_active,
                        rgb.note,
                        rs.rule_type,
                        rs.rule_name
                    FROM leaudit_rule_group_bindings rgb
                    JOIN leaudit_evaluation_point_groups child ON child.id = rgb.group_id
                    LEFT JOIN leaudit_document_types dt ON dt.id = child.document_type_id
                    JOIN leaudit_rule_sets rs ON rs.id = rgb.rule_set_id
                    WHERE rgb.id = :bid
                      AND rgb.deleted_at IS NULL
                    LIMIT 1
                    """
                ),
                {"bid": BindingId},
            )
            GroupRow = GroupBinding.mappings().first()
            if GroupRow:
                await Session.execute(
                    text(
                        """
                        UPDATE leaudit_rule_group_bindings
                        SET priority = :priority,
                            is_active = :is_active,
                            note = :note,
                            updated_at = NOW()
                        WHERE id = :binding_id
                        """
                    ),
                    {
                        "binding_id": BindingId,
                        "priority": Priority if Priority is not None else int(GroupRow.get("priority") or 0),
                        "is_active": IsActive if IsActive is not None else bool(GroupRow.get("is_active", True)),
                        "note": Note if Note is not None else GroupRow.get("note"),
                    },
                )
                await sync_doc_type_bindings_from_group(Session, int(GroupRow["group_id"]))
                await Session.commit()
                refreshed = (
                    await Session.execute(
                        text(
                            """
                            SELECT
                                rgb.id,
                                rgb.group_id,
                                child.document_type_id AS doc_type_id,
                                dt.code AS doc_type_code,
                                rgb.rule_set_id,
                                rgb.priority,
                                rgb.is_active,
                                rgb.note,
                                rs.rule_type,
                                rs.rule_name
                            FROM leaudit_rule_group_bindings rgb
                            JOIN leaudit_evaluation_point_groups child ON child.id = rgb.group_id
                            LEFT JOIN leaudit_document_types dt ON dt.id = child.document_type_id
                            JOIN leaudit_rule_sets rs ON rs.id = rgb.rule_set_id
                            WHERE rgb.id = :bid
                            LIMIT 1
                            """
                        ),
                        {"bid": BindingId},
                    )
                ).mappings().first()
                return RuleBindingVO(
                    id=int(refreshed["id"]),
                    docTypeId=int(refreshed["doc_type_id"]),
                    docTypeCode=refreshed["doc_type_code"],
                    ruleSetId=int(refreshed["rule_set_id"]),
                    ruleType=refreshed["rule_type"],
                    ruleName=refreshed["rule_name"],
                    bindingMode=BindingMode or "explicit",
                    priority=int(refreshed["priority"]),
                    isActive=bool(refreshed["is_active"]),
                    note=refreshed["note"],
                )

            raise LeauditException(
                StatusCodeEnum.HTTP_404_NOT_FOUND,
                "绑定记录不存在或已迁移到新分组绑定，请刷新列表后重试",
            )

    async def DeleteBinding(self, BindingId: int) -> None:
        """删除规则类型绑定；若绑定ID来自新分组绑定，则优先删除新表。"""
        async with GetAsyncSession() as Session:
            GroupBinding = await Session.execute(
                text(
                    """
                    SELECT id, group_id
                    FROM leaudit_rule_group_bindings
                    WHERE id = :bid
                      AND deleted_at IS NULL
                    LIMIT 1
                    """
                ),
                {"bid": BindingId},
            )
            GroupRow = GroupBinding.mappings().first()
            if GroupRow:
                await Session.execute(
                    text("UPDATE leaudit_rule_group_bindings SET deleted_at = NOW(), updated_at = NOW() WHERE id = :bid"),
                    {"bid": BindingId},
                )
                await sync_doc_type_bindings_from_group(Session, int(GroupRow["group_id"]))
                await Session.commit()
                return

            raise LeauditException(
                StatusCodeEnum.HTTP_404_NOT_FOUND,
                "绑定记录不存在或已迁移到新分组绑定，请刷新列表后重试",
            )

    async def _SwitchVersion(
        self,
        *,
        RuleType: str,
        VersionId: int,
        TargetStatus: str,
        OperatorUserId: int | None = None,
        Rollback: bool = False,
    ) -> RuleVersionVO:
        """切换当前生效版本。"""
        async with GetAsyncSession() as Session:
            RuleSet = await self._GetRuleSetByType(Session, RuleType)
            if not RuleSet:
                raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则集不存在")

            VersionRow = await self._GetVersion(Session, VersionId)
            if not VersionRow or int(VersionRow["rule_set_id"]) != int(RuleSet["id"]):
                raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则版本不存在或不属于当前规则集")

            await Session.execute(
                text(
                    """
                    UPDATE leaudit_rule_versions
                    SET
                        status = CASE
                            WHEN id = :version_id THEN :target_status
                            WHEN status = 'published' THEN :previous_status
                            ELSE status
                        END,
                        publisher_user_id = CASE
                            WHEN id = :version_id THEN :operator_user_id
                            ELSE publisher_user_id
                        END,
                        published_at = CASE
                            WHEN id = :version_id THEN now()
                            ELSE published_at
                        END,
                        updated_at = now()
                    WHERE rule_set_id = :rule_set_id
                    """
                ),
                {
                    "version_id": VersionId,
                    "target_status": TargetStatus,
                    "previous_status": "rollback" if Rollback else "deprecated",
                    "operator_user_id": OperatorUserId,
                    "rule_set_id": RuleSet["id"],
                },
            )
            await Session.execute(
                text(
                    """
                    UPDATE leaudit_rule_sets
                    SET
                        current_version_id = :version_id,
                        status = 'active',
                        updated_at = now()
                    WHERE id = :rule_set_id
                    """
                ),
                {
                    "version_id": VersionId,
                    "rule_set_id": RuleSet["id"],
                },
            )
            await Session.commit()

            self.InvalidateCaches()
            try:
                from fastapi_modules.fastapi_leaudit.services.impl.ruleConfigServiceImpl import GetRuleConfigServiceSingleton

                RuleConfigService = GetRuleConfigServiceSingleton()
                RuleConfigService.InvalidateSummaryCaches()
                await RuleConfigService.WarmPackSummaries(force=False)
            except Exception as exc:
                logging.getLogger("RULE").warning("刷新规则配置摘要缓存失败: %s", exc)

            VersionRow = await self._GetVersion(Session, VersionId)
            return self._BuildRuleVersionVo(VersionRow)

    async def _GetRuleSetByType(self, Session, RuleType: str):
        """按规则类型查询规则集。"""
        Result = await Session.execute(
            text(
                """
                SELECT id, rule_type, rule_name, domain_type, current_version_id, status
                FROM leaudit_rule_sets
                WHERE rule_type = :rule_type
                  AND deleted_at IS NULL
                LIMIT 1
                """
            ),
            {"rule_type": RuleType},
        )
        return Result.mappings().first()

    async def _GetVersion(self, Session, VersionId: int):
        """按版本 ID 查询规则版本。"""
        Result = await Session.execute(
            text(
                """
                SELECT
                    id,
                    rule_set_id,
                    version_no,
                    status,
                    oss_url,
                    change_note,
                    published_at
                FROM leaudit_rule_versions
                WHERE id = :version_id
                LIMIT 1
                """
            ),
            {"version_id": VersionId},
        )
        return Result.mappings().first()

    def _BuildRuleVersionVo(self, Row) -> RuleVersionVO:
        """构造规则版本响应。"""
        return RuleVersionVO(
            id=int(Row["id"]),
            ruleSetId=int(Row["rule_set_id"]),
            versionNo=Row["version_no"],
            status=Row["status"],
            ossUrl=Row["oss_url"],
            changeNote=Row["change_note"],
            publishedAt=Row["published_at"].isoformat() if Row["published_at"] else None,
        )

    def _ResolveDomainType(self, RuleType: str) -> str:
        """按规则类型推导领域类型。"""
        Prefix = RuleType.split(".", 1)[0]
        Mapping = {
            "contract": "contract",
            "admin_license": "admin_license",
            "legal_doc": "legal_doc",
        }
        return Mapping.get(Prefix, Prefix or "unknown")


_RULE_SERVICE_SINGLETON: RuleServiceImpl | None = None


def GetRuleServiceSingleton() -> RuleServiceImpl:
    """返回共享规则服务实例，供控制器与聚合服务复用缓存。"""
    global _RULE_SERVICE_SINGLETON
    if _RULE_SERVICE_SINGLETON is None:
        _RULE_SERVICE_SINGLETON = RuleServiceImpl()
    return _RULE_SERVICE_SINGLETON
