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

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
from fastapi_modules.fastapi_leaudit.services.impl.ruleTenantScope import normalize_scoped_tenant_code, pick_effective_scoped_row
from fastapi_modules.fastapi_leaudit.services.impl.ssoUserCompat import SsoUserCompat
from fastapi_modules.fastapi_leaudit.services.impl.tenantResolver import TenantResolver


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.TenantResolver = TenantResolver()
        self._list_sets_cache = self.__class__._GLOBAL_LIST_SETS_CACHE
        self._rule_count_cache = self.__class__._GLOBAL_RULE_COUNT_CACHE
        self._entry_module_tenant_table_exists_cache: bool | None = None
        self._column_exists_cache: dict[str, bool] = {}

    @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, CurrentUserId: int | None = None) -> list[RuleSetVO]:
        """列出所有规则集。"""
        now = time.monotonic()
        cached = self.__class__._GLOBAL_LIST_SETS_CACHE
        if CurrentUserId is None and cached and now - cached[0] <= 30:
            return cached[1]

        async with GetAsyncSession() as Session:
            current_user = await self._get_current_user_context(Session, CurrentUserId)
            if await self._column_exists(Session, "leaudit_rule_sets", "tenant_code"):
                rows = await self._load_scoped_rule_set_rows(Session, current_user=current_user)
            else:
                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"],
                effectiveTenantCode=str(Row.get("tenant_code") or "").strip() or None,
                effectiveScopeType="PUBLIC"
                if str(Row.get("tenant_code") or "").strip().upper() == "PUBLIC"
                else "PROVINCIAL"
                if str(Row.get("tenant_code") or "").strip().upper() in {"", "PROVINCIAL"}
                else "TENANT",
                isInherited=bool(current_user)
                and not bool(current_user.get("is_global"))
                and bool(str(current_user.get("tenant_code") or "").strip())
                and str(Row.get("tenant_code") or "").strip().upper()
                not in {"", str(current_user.get("tenant_code") or "").strip().upper()},
                sourceRuleSetId=int(Row["source_rule_set_id"]) if Row.get("source_rule_set_id") is not None else None,
                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)
        if CurrentUserId is None:
            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, CurrentUserId: int | None = None) -> list[RuleVersionVO]:
        """获取规则集的所有版本。"""
        async with GetAsyncSession() as Session:
            current_user = await self._get_current_user_context(Session, CurrentUserId)
            if await self._column_exists(Session, "leaudit_rule_sets", "tenant_code"):
                rule_set_row = await self._get_effective_rule_set_row_by_type(Session, RuleType, current_user=current_user)
                if not rule_set_row:
                    return []
                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
                        WHERE rv.rule_set_id = :rule_set_id
                          AND rv.deleted_at IS NULL
                        ORDER BY rv.version_seq DESC, rv.id DESC
                        """
                    ),
                    {"rule_set_id": int(rule_set_row["id"])},
                )
            else:
                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
                          AND rv.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, CurrentUserId: int | None = None) -> RuleContentVO:
        """获取指定版本的规则正文。"""
        async with GetAsyncSession() as Session:
            current_user = await self._get_current_user_context(Session, CurrentUserId)
            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
                      AND rv.deleted_at IS NULL
                    LIMIT 1
                    """
                ),
                {"version_id": VersionId},
            )
            Row = Result.mappings().first()
            if not Row:
                raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则版本不存在")
            if await self._column_exists(Session, "leaudit_rule_sets", "tenant_code"):
                effective_rule_set = await self._get_effective_rule_set_row_by_type(
                    Session,
                    str(Row["rule_type"] or ""),
                    current_user=current_user,
                )
                if effective_rule_set is None or int(effective_rule_set["id"]) != int(Row["rule_set_id"]):
                    raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前租户不可访问该规则版本")

        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 [],
        )

    def _pick_writable_rule_set_row(
        self,
        rows: list[dict[str, object]],
        *,
        current_user: dict[str, object] | None,
    ) -> dict[str, object] | None:
        if not rows:
            return None
        if current_user is None:
            return dict(rows[0])
        current_user_tenant = str(current_user.get("tenant_code") or "").strip().upper()
        if current_user.get("is_global") and not current_user_tenant:
            effective = pick_effective_scoped_row(rows, "PROVINCIAL")
            return dict(effective) if effective is not None else dict(rows[0])
        effective = pick_effective_scoped_row(rows, current_user_tenant or None)
        if effective is None:
            return None
        normalized = str(effective.get("tenant_code") or "").strip().upper() or "PROVINCIAL"
        if normalized == "PUBLIC":
            raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前租户不能直接修改公共规则资产")
        return dict(effective)

    def _assert_version_belongs_to_writable_rule_set(
        self,
        version_row: dict[str, object] | None,
        writable_rule_set: dict[str, object] | None,
    ) -> None:
        if version_row is None or writable_rule_set is None:
            raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则版本不存在或当前租户无可写规则集")
        if int(version_row["rule_set_id"]) != int(writable_rule_set["id"]):
            raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前租户不能发布或回滚其他租户的规则版本")

    def _assert_rollback_target(
        self,
        version_row: dict[str, object] | None,
        rule_set_row: dict[str, object] | None,
    ) -> None:
        if version_row is None or rule_set_row is None:
            raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则版本不存在或当前租户无可写规则集")
        current_version_id = rule_set_row.get("current_version_id")
        if current_version_id is not None and int(version_row["id"]) == int(current_version_id):
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前版本已经生效，无需回滚")
        status = str(version_row.get("status") or "").strip().lower()
        if status in {"draft", "validated"}:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "草稿版本不能作为回滚目标，请先发布或选择历史版本")

    async def CreateVersion(
        self,
        RuleType: str,
        YamlText: str,
        ChangeNote: str | None = None,
        EditorUserId: int | None = None,
        CurrentUserId: 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:
            current_user = await self._get_current_user_context(Session, CurrentUserId)
            use_tenant_scope = await self._column_exists(Session, "leaudit_rule_sets", "tenant_code")
            self._assert_rule_tenant_schema_ready_for_write(use_tenant_scope=use_tenant_scope, current_user=current_user)
            writable_tenant_code: str | None = None
            writable_scope_type = "PROVINCIAL"
            source_rule_set_id: int | None = None
            source_rule_set_binding_ids: list[int] = []

            if use_tenant_scope:
                candidate_rows = await self._load_rule_set_rows_by_type(Session, RuleType)
                writable_rule_set = self._pick_writable_rule_set_row(candidate_rows, current_user=current_user)
                RuleSet = writable_rule_set
                if current_user and not current_user.get("is_global"):
                    writable_tenant_code = str(current_user.get("tenant_code") or "").strip().upper() or None
                    writable_scope_type = "TENANT"
                elif RuleSet is not None:
                    writable_tenant_code = str(RuleSet.get("tenant_code") or "").strip().upper() or "PROVINCIAL"
                    writable_scope_type = "PROVINCIAL" if writable_tenant_code != "PUBLIC" else "PUBLIC"
                if RuleSet is not None:
                    normalized_rule_set_tenant = str(RuleSet.get("tenant_code") or "").strip().upper() or "PROVINCIAL"
                    if (
                        writable_scope_type == "TENANT"
                        and writable_tenant_code
                        and normalized_rule_set_tenant != writable_tenant_code
                    ):
                        source_rule_set_id = int(RuleSet["id"])
                        source_rule_set_binding_ids = await self._load_source_group_binding_ids(Session, source_rule_set_id)
                        RuleSet = None
            else:
                RuleSet = await self._GetRuleSetByType(Session, RuleType)
            if not RuleSet:
                if use_tenant_scope:
                    CreateResult = await Session.execute(
                        text(
                            """
                            INSERT INTO leaudit_rule_sets (
                                rule_type,
                                rule_name,
                                domain_type,
                                description,
                                region,
                                status,
                                is_builtin,
                                owner_user_id,
                                tenant_code,
                                scope_type,
                                source_rule_set_id,
                                tenant_name_snapshot
                            ) VALUES (
                                :rule_type,
                                :rule_name,
                                :domain_type,
                                :description,
                                :region,
                                'draft',
                                false,
                                :owner_user_id,
                                :tenant_code,
                                :scope_type,
                                :source_rule_set_id,
                                :tenant_name_snapshot
                            )
                            RETURNING id, rule_type, rule_name, domain_type, current_version_id, status, tenant_code, source_rule_set_id
                        """
                    ),
                        {
                            "rule_type": RuleType,
                            "rule_name": RulesFile.metadata.name,
                            "domain_type": DomainType,
                            "description": RulesFile.metadata.description,
                            "region": self._legacy_region_for_scope(writable_tenant_code, writable_scope_type),
                            "owner_user_id": EditorUserId,
                            "tenant_code": writable_tenant_code or "PROVINCIAL",
                            "scope_type": writable_scope_type,
                            "source_rule_set_id": source_rule_set_id,
                            "tenant_name_snapshot": str(current_user.get("tenant_name") or "") if current_user else None,
                        },
                    )
                else:
                    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()
                if use_tenant_scope and writable_scope_type == "TENANT" and source_rule_set_binding_ids:
                    await self._clone_source_group_bindings_for_tenant(
                        Session,
                        source_binding_ids=source_rule_set_binding_ids,
                        tenant_rule_set_id=int(RuleSet["id"]),
                        current_user=current_user,
                    )
            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}"
            source_version_id: int | None = None
            if source_rule_set_id is not None:
                source_version_id = await self._load_latest_version_id_by_rule_set(Session, source_rule_set_id)
            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,
                },
            )
            version_row = VersionResult.mappings().first()
            if use_tenant_scope and await self._column_exists(Session, "leaudit_rule_versions", "tenant_code_snapshot"):
                snapshot = self._build_version_scope_snapshot(
                    writable_tenant_code=writable_tenant_code,
                    writable_scope_type=writable_scope_type,
                    rule_set_row=dict(RuleSet),
                    source_version_id=source_version_id,
                )
                await Session.execute(
                    text(
                        """
                        UPDATE leaudit_rule_versions
                        SET
                            tenant_code_snapshot = :tenant_code_snapshot,
                            scope_type_snapshot = :scope_type_snapshot,
                            source_version_id = :source_version_id
                        WHERE id = :version_id
                        """
                    ),
                    {
                        "tenant_code_snapshot": snapshot["tenant_code_snapshot"],
                        "scope_type_snapshot": snapshot["scope_type_snapshot"],
                        "source_version_id": snapshot["source_version_id"],
                        "version_id": int(version_row["id"]),
                    },
                )
            await Session.commit()
            return self._BuildRuleVersionVo(version_row)

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

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

    async def ListBindings(
        self,
        RuleType: str | None = None,
        Region: str | None = None,
        CurrentUserId: int | None = None,
    ) -> list[RuleBindingVO]:
        """列出规则类型绑定，只读取新分组绑定。"""
        async with GetAsyncSession() as Session:
            current_user = await self._get_current_user_context(Session, CurrentUserId)
            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"]
            filters.append(
                await self._entry_module_tenant_scope_sql(
                    session=Session,
                    entry_module_expr="COALESCE(child.entry_module_id, parent.entry_module_id, dt.entry_module_id)",
                    current_user=current_user,
                    params=params,
                    param_prefix="rule_binding_scope",
                )
            )
            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
                    LEFT JOIN leaudit_evaluation_point_groups parent ON parent.id = child.pid AND parent.deleted_at IS NULL
                    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,
        CurrentUserId: int | None = None,
    ) -> RuleBindingVO:
        """创建规则类型绑定；若文档类型唯一对应一个二级分组，则优先写入新分组绑定。"""
        async with GetAsyncSession() as Session:
            current_user = await self._get_current_user_context(Session, CurrentUserId)
            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, "规则集不存在")

            await self._assert_document_type_access(Session, DocTypeId, current_user)
            GroupId = await self._resolve_unique_accessible_child_group_id(Session, DocTypeId, current_user)
            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_accessible_child_groups(Session, DocTypeId, current_user)
            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,
        CurrentUserId: int | None = None,
    ) -> RuleBindingVO:
        """更新规则类型绑定；若绑定ID来自新分组绑定，则优先更新新表。"""
        async with GetAsyncSession() as Session:
            current_user = await self._get_current_user_context(Session, CurrentUserId)
            GroupRow = await self._get_binding_row(Session, BindingId, current_user)
            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 self._get_binding_row(Session, BindingId, current_user)
                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, CurrentUserId: int | None = None) -> None:
        """删除规则类型绑定；若绑定ID来自新分组绑定，则优先删除新表。"""
        async with GetAsyncSession() as Session:
            current_user = await self._get_current_user_context(Session, CurrentUserId)
            GroupRow = await self._get_binding_row(Session, BindingId, current_user)
            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,
        CurrentUserId: int | None = None,
        Rollback: bool = False,
    ) -> RuleVersionVO:
        """切换当前生效版本。"""
        async with GetAsyncSession() as Session:
            current_user = await self._get_current_user_context(Session, CurrentUserId)
            use_tenant_scope = await self._column_exists(Session, "leaudit_rule_sets", "tenant_code")
            self._assert_rule_tenant_schema_ready_for_write(use_tenant_scope=use_tenant_scope, current_user=current_user)
            if use_tenant_scope:
                candidate_rows = await self._load_rule_set_rows_by_type(Session, RuleType)
                RuleSet = self._pick_writable_rule_set_row(candidate_rows, current_user=current_user)
            else:
                RuleSet = await self._GetRuleSetByType(Session, RuleType)
            if not RuleSet:
                raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则集不存在")

            VersionRow = await self._GetVersion(Session, VersionId)
            self._assert_version_belongs_to_writable_rule_set(
                dict(VersionRow) if VersionRow else None,
                dict(RuleSet) if RuleSet else None,
            )
            if Rollback:
                self._assert_rollback_target(
                    dict(VersionRow) if VersionRow else None,
                    dict(RuleSet) if RuleSet else None,
                )

            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 _load_rule_set_rows_by_type(self, Session, RuleType: str) -> list[dict[str, object]]:
        if not await self._column_exists(Session, "leaudit_rule_sets", "tenant_code"):
            row = await self._GetRuleSetByType(Session, RuleType)
            return [dict(row)] if row else []
        Result = await Session.execute(
            text(
                """
                SELECT
                    id,
                    rule_type,
                    rule_name,
                    domain_type,
                    current_version_id,
                    status,
                    COALESCE(NULLIF(BTRIM(tenant_code), ''), 'PROVINCIAL') AS tenant_code,
                    source_rule_set_id
                FROM leaudit_rule_sets
                WHERE rule_type = :rule_type
                  AND deleted_at IS NULL
                ORDER BY id DESC
                """
            ),
            {"rule_type": RuleType},
        )
        return [dict(row) for row in Result.mappings().all()]

    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
                  AND deleted_at IS NULL
                LIMIT 1
                """
            ),
            {"version_id": VersionId},
        )
        return Result.mappings().first()

    async def _load_latest_version_id_by_rule_set(self, Session, RuleSetId: int) -> int | None:
        row = (
            await Session.execute(
                text(
                    """
                    SELECT id
                    FROM leaudit_rule_versions
                    WHERE rule_set_id = :rule_set_id
                      AND deleted_at IS NULL
                    ORDER BY version_seq DESC, id DESC
                    LIMIT 1
                    """
                ),
                {"rule_set_id": RuleSetId},
            )
        ).mappings().first()
        return int(row["id"]) if row and row.get("id") is not None else None

    def _build_version_scope_snapshot(
        self,
        *,
        writable_tenant_code: str | None,
        writable_scope_type: str,
        rule_set_row: dict[str, object],
        source_version_id: int | None,
    ) -> dict[str, object | None]:
        return {
            "tenant_code_snapshot": writable_tenant_code or str(rule_set_row.get("tenant_code") or "") or "PROVINCIAL",
            "scope_type_snapshot": writable_scope_type,
            "source_version_id": source_version_id,
        }

    def _legacy_region_for_scope(self, tenant_code: str | None, scope_type: str | None) -> str:
        """兼容旧唯一约束 rule_type + region；租户私有规则集不能再共用 default。"""
        normalized_tenant = normalize_scoped_tenant_code(str(tenant_code or ""), default="")
        normalized_scope = str(scope_type or "").strip().upper()
        if normalized_scope == "TENANT" and normalized_tenant:
            return normalized_tenant
        if normalized_tenant == "PUBLIC" or normalized_scope == "PUBLIC":
            return "PUBLIC"
        return "default"

    def _assert_rule_tenant_schema_ready_for_write(
        self,
        *,
        use_tenant_scope: bool,
        current_user: dict[str, object] | None,
    ) -> None:
        """租户用户不能在缺少规则域租户列时写共享规则集。"""
        if use_tenant_scope:
            return
        if current_user is None or current_user.get("is_global"):
            return
        if str(current_user.get("tenant_code") or "").strip():
            raise LeauditException(
                StatusCodeEnum.HTTP_409_CONFLICT,
                "规则域租户隔离表结构未就绪，已拒绝写入共享规则集；请先执行 schema_rule_domain_tenant_phase1.sql",
            )

    def _build_tenant_binding_clone_payload(
        self,
        *,
        current_user: dict[str, object],
        source_binding: dict[str, object],
        tenant_rule_set_id: int,
    ) -> dict[str, object | None]:
        tenant_code = normalize_scoped_tenant_code(str(current_user.get("tenant_code") or ""), default="")
        if not tenant_code:
            raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前租户上下文缺失，不能派生规则绑定")
        return {
            "group_id": int(source_binding["group_id"]),
            "rule_set_id": int(tenant_rule_set_id),
            "tenant_code": tenant_code,
            "scope_type": "TENANT",
            "tenant_name_snapshot": str(current_user.get("tenant_name") or "").strip() or None,
            "priority": int(source_binding.get("priority") or 0),
            "note": "由租户规则集派生自动补绑",
        }

    async def _load_source_group_binding_ids(self, Session, source_rule_set_id: int) -> list[int]:
        if not await self._column_exists(Session, "leaudit_rule_group_bindings", "tenant_code"):
            return []
        rows = (
            await Session.execute(
                text(
                    """
                    SELECT id
                    FROM leaudit_rule_group_bindings
                    WHERE rule_set_id = :rule_set_id
                      AND deleted_at IS NULL
                      AND is_active = TRUE
                    ORDER BY priority DESC, id ASC
                    """
                ),
                {"rule_set_id": source_rule_set_id},
            )
        ).mappings().all()
        return [int(row["id"]) for row in rows]

    async def _clone_source_group_bindings_for_tenant(
        self,
        Session,
        *,
        source_binding_ids: list[int],
        tenant_rule_set_id: int,
        current_user: dict[str, object] | None,
    ) -> None:
        if not source_binding_ids or not current_user or current_user.get("is_global"):
            return
        rows = (
            await Session.execute(
                text(
                    """
                    SELECT id, group_id, priority, note
                    FROM leaudit_rule_group_bindings
                    WHERE id = ANY(:binding_ids)
                      AND deleted_at IS NULL
                      AND is_active = TRUE
                    ORDER BY priority DESC, id ASC
                    """
                ),
                {"binding_ids": source_binding_ids},
            )
        ).mappings().all()
        for row in rows:
            payload = self._build_tenant_binding_clone_payload(
                current_user=current_user,
                source_binding=dict(row),
                tenant_rule_set_id=tenant_rule_set_id,
            )
            await Session.execute(
                text(
                    """
                    INSERT INTO leaudit_rule_group_bindings (
                        group_id,
                        rule_set_id,
                        tenant_code,
                        scope_type,
                        tenant_name_snapshot,
                        priority,
                        is_active,
                        note,
                        created_at,
                        updated_at
                    )
                    SELECT
                        :group_id,
                        :rule_set_id,
                        CAST(:tenant_code AS varchar),
                        :scope_type,
                        :tenant_name_snapshot,
                        :priority,
                        TRUE,
                        :note,
                        NOW(),
                        NOW()
                    WHERE NOT EXISTS (
                        SELECT 1
                        FROM leaudit_rule_group_bindings
                        WHERE group_id = :group_id
                          AND tenant_code = CAST(:tenant_code AS varchar)
                          AND deleted_at IS NULL
                    )
                    """
                ),
                payload,
            )

    async def _column_exists(self, Session, table_name: str, column_name: str) -> bool:
        cache_key = f"{table_name}.{column_name}"
        cached = self._column_exists_cache.get(cache_key)
        if cached is not None:
            return cached
        exists = bool(
            (
                await Session.execute(
                    text(
                        """
                        SELECT EXISTS (
                            SELECT 1
                            FROM information_schema.columns
                            WHERE table_schema = current_schema()
                              AND table_name = :table_name
                              AND column_name = :column_name
                        )
                        """
                    ),
                    {"table_name": table_name, "column_name": column_name},
                )
            ).scalar_one()
        )
        if exists:
            self._column_exists_cache[cache_key] = exists
        return exists

    async def _load_scoped_rule_set_rows(
        self,
        Session,
        *,
        current_user: dict[str, object] | None,
    ) -> list[dict[str, object]]:
        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,
                    COALESCE(NULLIF(BTRIM(rs.tenant_code), ''), 'PROVINCIAL') AS tenant_code,
                    rs.source_rule_set_id
                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.rule_type ASC, rs.id DESC
                """
            )
        )
        rows = [dict(row) for row in Result.mappings().all()]
        current_user_tenant = str((current_user or {}).get("tenant_code") or "").strip().upper()
        if current_user is None or (current_user.get("is_global") and not current_user_tenant):
            return rows

        grouped: dict[str, list[dict[str, object]]] = {}
        for row in rows:
            grouped.setdefault(str(row.get("rule_type") or ""), []).append(row)

        effective_rows: list[dict[str, object]] = []
        for group_rows in grouped.values():
            effective = pick_effective_scoped_row(group_rows, current_user_tenant or None)
            if effective is not None:
                effective_rows.append(dict(effective))
        effective_rows.sort(key=lambda item: int(item["id"]), reverse=True)
        return effective_rows

    async def _get_effective_rule_set_row_by_type(
        self,
        Session,
        RuleType: str,
        *,
        current_user: dict[str, object] | None,
    ) -> dict[str, object] | None:
        if not await self._column_exists(Session, "leaudit_rule_sets", "tenant_code"):
            return await self._GetRuleSetByType(Session, RuleType)

        Result = await Session.execute(
            text(
                """
                SELECT
                    rs.id,
                    rs.rule_type,
                    rs.rule_name,
                    rs.domain_type,
                    rs.current_version_id,
                    rs.status,
                    COALESCE(NULLIF(BTRIM(rs.tenant_code), ''), 'PROVINCIAL') AS tenant_code,
                    rs.source_rule_set_id
                FROM leaudit_rule_sets rs
                WHERE rs.rule_type = :rule_type
                  AND rs.deleted_at IS NULL
                ORDER BY rs.id DESC
                """
            ),
            {"rule_type": RuleType},
        )
        rows = [dict(row) for row in Result.mappings().all()]
        if not rows:
            return None
        current_user_tenant = str((current_user or {}).get("tenant_code") or "").strip().upper()
        if current_user is None or (current_user.get("is_global") and not current_user_tenant):
            return rows[0]
        effective = pick_effective_scoped_row(rows, current_user_tenant or None)
        return dict(effective) if effective is not None else None

    async def _get_current_user_context(self, Session, CurrentUserId: int | None) -> dict[str, object] | None:
        if CurrentUserId is None:
            return None
        sso_user_columns = await SsoUserCompat.get_columns(Session)
        tenant_code_expr = SsoUserCompat.raw_optional_column(
            sso_user_columns,
            alias="u",
            column="tenant_code",
        )
        tenant_name_expr = SsoUserCompat.raw_optional_column(
            sso_user_columns,
            alias="u",
            column="tenant_name",
        )
        row = (
            await Session.execute(
                text(
                    f"""
                    SELECT
                        u.id,
                        COALESCE(u.area, '') AS area,
                        COALESCE(MAX({tenant_code_expr}), '') AS tenant_code,
                        COALESCE(MAX({tenant_name_expr}), '') AS tenant_name,
                        COALESCE(bool_or(r.role_key IN ('super_admin', 'provincial_admin')), FALSE) AS is_global
                    FROM sso_users u
                    LEFT JOIN user_role ur ON ur.user_id = u.id
                    LEFT JOIN roles r ON r.id = ur.role_id
                    WHERE u.id = :user_id
                      AND u.deleted_at IS NULL
                      AND u.status = 0
                    GROUP BY u.id, u.area
                    """
                ),
                {"user_id": CurrentUserId},
            )
        ).mappings().first()
        if not row:
            raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "当前用户不存在或已停用")
        tenant = await self.TenantResolver.ResolveUserContext(
            Area=str(row["area"] or ""),
            TenantCode=str(row.get("tenant_code") or "") or None,
            TenantName=str(row.get("tenant_name") or "") or None,
            Source="rule_binding_user_context",
        )
        return {
            "id": int(row["id"]),
            "tenant_code": tenant.tenant_code or (str(row.get("tenant_code") or "") or None),
            "tenant_name": tenant.tenant_name or (str(row.get("tenant_name") or "") or None),
            "is_global": bool(row["is_global"]),
        }

    async def _entry_module_tenant_table_exists(self, session) -> bool:
        if self._entry_module_tenant_table_exists_cache is not None:
            return self._entry_module_tenant_table_exists_cache
        exists = bool(
            (
                await session.execute(
                    text(
                        """
                        SELECT EXISTS (
                            SELECT 1
                            FROM information_schema.tables
                            WHERE table_schema = current_schema()
                              AND table_name = 'leaudit_entry_module_tenants'
                        )
                        """
                    )
                )
            ).scalar_one()
        )
        self._entry_module_tenant_table_exists_cache = exists
        return exists

    async def _entry_module_tenant_scope_sql(
        self,
        *,
        session,
        entry_module_expr: str,
        current_user: dict[str, object] | None,
        params: dict[str, object],
        param_prefix: str,
    ) -> str:
        if current_user is None or current_user.get("is_global"):
            return "1=1"
        if not await self._entry_module_tenant_table_exists(session):
            return "1=0"
        params[f"{param_prefix}_tenant_code"] = str(current_user.get("tenant_code") or "").strip()
        return f"""
            EXISTS (
                SELECT 1
                FROM leaudit_entry_module_tenants emt
                WHERE emt.entry_module_id = {entry_module_expr}
                  AND emt.deleted_at IS NULL
                  AND emt.is_enabled = TRUE
                  AND (
                      emt.tenant_code = :{param_prefix}_tenant_code
                      OR emt.tenant_code = 'PUBLIC'
                  )
            )
        """

    async def _assert_entry_module_access(self, Session, EntryModuleId: int | None, CurrentUser: dict[str, object] | None) -> None:
        if CurrentUser is None or CurrentUser.get("is_global"):
            return
        if EntryModuleId is None:
            raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前资源未绑定入口模块，当前租户不可访问")
        params: dict[str, object] = {"entry_module_id": int(EntryModuleId)}
        access_filter = await self._entry_module_tenant_scope_sql(
            session=Session,
            entry_module_expr="em.id",
            current_user=CurrentUser,
            params=params,
            param_prefix="assert_rule_binding_scope",
        )
        exists = (
            await Session.execute(
                text(
                    f"""
                    SELECT em.id
                    FROM leaudit_entry_modules em
                    WHERE em.id = :entry_module_id
                      AND em.deleted_at IS NULL
                      AND {access_filter}
                    LIMIT 1
                    """
                ),
                params,
            )
        ).scalar_one_or_none()
        if exists is None:
            raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前租户不可访问该入口模块")

    async def _assert_document_type_access(self, Session, DocTypeId: int, CurrentUser: dict[str, object] | None) -> None:
        row = (
            await Session.execute(
                text(
                    """
                    SELECT id, entry_module_id
                    FROM leaudit_document_types
                    WHERE id = :doc_type_id
                      AND deleted_at IS NULL
                    LIMIT 1
                    """
                ),
                {"doc_type_id": DocTypeId},
            )
        ).mappings().first()
        if not row:
            raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "文档类型不存在")
        await self._assert_entry_module_access(
            Session,
            int(row["entry_module_id"]) if row.get("entry_module_id") is not None else None,
            CurrentUser,
        )

    async def _resolve_unique_accessible_child_group_id(self, Session, DocTypeId: int, CurrentUser: dict[str, object] | None) -> int | None:
        params: dict[str, object] = {"doc_type_id": DocTypeId}
        access_filter = await self._entry_module_tenant_scope_sql(
            session=Session,
            entry_module_expr="COALESCE(child.entry_module_id, parent.entry_module_id, dt.entry_module_id)",
            current_user=CurrentUser,
            params=params,
            param_prefix="unique_child_scope",
        )
        row = (
            await Session.execute(
                text(
                    f"""
                    SELECT CASE WHEN COUNT(*) = 1 THEN MIN(child.id) END AS group_id
                    FROM leaudit_evaluation_point_groups child
                    LEFT JOIN leaudit_evaluation_point_groups parent ON parent.id = child.pid AND parent.deleted_at IS NULL
                    LEFT JOIN leaudit_document_types dt ON dt.id = child.document_type_id
                    WHERE child.document_type_id = :doc_type_id
                      AND child.deleted_at IS NULL
                      AND COALESCE(child.pid, 0) <> 0
                      AND {access_filter}
                    """
                ),
                params,
            )
        ).mappings().first()
        return int(row["group_id"]) if row and row.get("group_id") is not None else None

    async def _count_accessible_child_groups(self, Session, DocTypeId: int, CurrentUser: dict[str, object] | None) -> int:
        params: dict[str, object] = {"doc_type_id": DocTypeId}
        access_filter = await self._entry_module_tenant_scope_sql(
            session=Session,
            entry_module_expr="COALESCE(child.entry_module_id, parent.entry_module_id, dt.entry_module_id)",
            current_user=CurrentUser,
            params=params,
            param_prefix="count_child_scope",
        )
        row = (
            await Session.execute(
                text(
                    f"""
                    SELECT COUNT(*) AS total
                    FROM leaudit_evaluation_point_groups child
                    LEFT JOIN leaudit_evaluation_point_groups parent ON parent.id = child.pid AND parent.deleted_at IS NULL
                    LEFT JOIN leaudit_document_types dt ON dt.id = child.document_type_id
                    WHERE child.document_type_id = :doc_type_id
                      AND child.deleted_at IS NULL
                      AND COALESCE(child.pid, 0) <> 0
                      AND {access_filter}
                    """
                ),
                params,
            )
        ).mappings().first()
        return int(row["total"] or 0) if row else 0

    async def _get_binding_row(self, Session, BindingId: int, CurrentUser: dict[str, object] | None):
        params: dict[str, object] = {"bid": BindingId}
        access_filter = await self._entry_module_tenant_scope_sql(
            session=Session,
            entry_module_expr="COALESCE(child.entry_module_id, parent.entry_module_id, dt.entry_module_id)",
            current_user=CurrentUser,
            params=params,
            param_prefix="binding_detail_scope",
        )
        row = (
            await Session.execute(
                text(
                    f"""
                    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_evaluation_point_groups parent ON parent.id = child.pid AND parent.deleted_at IS NULL
                    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
                      AND {access_filter}
                    LIMIT 1
                    """
                ),
                params,
            )
        ).mappings().first()
        if not row:
            raise LeauditException(
                StatusCodeEnum.HTTP_404_NOT_FOUND,
                "绑定记录不存在或当前租户不可访问，请刷新列表后重试",
            )
        return row

    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
