"""评查点分组服务实现（新链路：文档类型 -> 子类型 -> 规则集）。"""

from __future__ import annotations

from datetime import datetime
from typing import Any

from sqlalchemy import bindparam, text

from fastapi_common.fastapi_common_storage.oss_path_utils import OssPathUtils
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_modules.fastapi_leaudit.domian.Dto.evaluationPointGroupDto import (
    EvaluationPointGroupBatchDeleteDTO,
    EvaluationPointGroupBatchStatusDTO,
    EvaluationPointGroupBindingCreateDTO,
    EvaluationPointGroupBindingUpdateDTO,
    EvaluationPointGroupCreateDTO,
    EvaluationPointGroupRuleDraftCreateDTO,
    EvaluationPointGroupRebindDTO,
    EvaluationPointGroupUpdateDTO,
)
from fastapi_modules.fastapi_leaudit.domian.vo.evaluationPointGroupVo import (
    EvaluationPointGroupBatchDeleteVO,
    EvaluationPointGroupBatchStatusVO,
    EvaluationPointGroupDeleteVO,
    EvaluationPointGroupListVO,
    EvaluationPointGroupRuleDraftVO,
    EvaluationPointGroupRuleTemplateContextVO,
    EvaluationPointGroupRuleTemplateVO,
    EvaluationPointGroupRebindVO,
    EvaluationPointGroupVO,
    RuleGroupBindingVO,
)
from fastapi_modules.fastapi_leaudit.services.evaluationPointGroupService import IEvaluationPointGroupService
from fastapi_modules.fastapi_leaudit.services.impl.ruleGroupSupport import (
    bootstrap_rule_groups,
    ensure_rule_group_schema,
    sync_doc_type_bindings_from_group,
)
from fastapi_modules.fastapi_leaudit.services.impl.ruleServiceImpl import GetRuleServiceSingleton


class EvaluationPointGroupServiceImpl(IEvaluationPointGroupService):
    """评查点分组服务实现。"""

    def __init__(self) -> None:
        self.RuleService = GetRuleServiceSingleton()

    async def ListGroups(
        self,
        Name: str | None,
        Code: str | None,
        IsEnabled: bool | None,
        Pid: int | None,
        Page: int,
        PageSize: int,
    ) -> EvaluationPointGroupListVO:
        async with GetAsyncSession() as session:
            await self._ensure_ready(session)
            offset = max(Page - 1, 0) * PageSize
            filters = ["g.deleted_at IS NULL", "(COALESCE(g.pid, 0) <> 0 OR g.document_type_id IS NOT NULL OR g.entry_module_id IS NOT NULL)"]
            params: dict[str, Any] = {"limit": PageSize, "offset": offset}

            if Name:
                filters.append("g.name ILIKE :name")
                params["name"] = f"%{Name.strip()}%"
            if Code:
                filters.append("g.code ILIKE :code")
                params["code"] = f"%{Code.strip()}%"
            if IsEnabled is not None:
                filters.append("g.is_enabled = :is_enabled")
                params["is_enabled"] = IsEnabled
            if Pid is not None:
                filters.append("COALESCE(g.pid, 0) = :pid")
                params["pid"] = self._normalize_pid(Pid)

            where_clause = " AND ".join(filters)
            total = int(
                (
                    await session.execute(
                        text(f"SELECT COUNT(*) FROM leaudit_evaluation_point_groups g WHERE {where_clause}"),
                        params,
                    )
                ).scalar_one()
            )
            rows = (
                await session.execute(
                    text(
                        f"""
                        SELECT
                            g.id,
                            g.pid,
                            g.name,
                            g.code,
                            g.description,
                            g.document_type_id,
                            dt.name AS document_type_name,
                            COALESCE(g.entry_module_id, parent.entry_module_id, dt.entry_module_id) AS entry_module_id,
                            em.name AS entry_module_name,
                            g.sort_order,
                            g.is_enabled,
                            g.created_at,
                            g.updated_at,
                            COALESCE(bg.binding_count, 0) AS rule_count
                        FROM leaudit_evaluation_point_groups g
                        LEFT JOIN leaudit_document_types dt ON dt.id = g.document_type_id
                        LEFT JOIN leaudit_evaluation_point_groups parent ON parent.id = g.pid AND parent.deleted_at IS NULL
                        LEFT JOIN leaudit_entry_modules em ON em.id = COALESCE(g.entry_module_id, parent.entry_module_id, dt.entry_module_id)
                        LEFT JOIN (
                            SELECT group_id, COUNT(*)::int AS binding_count
                            FROM leaudit_rule_group_bindings
                            WHERE deleted_at IS NULL
                            GROUP BY group_id
                        ) bg ON bg.group_id = g.id
                        WHERE {where_clause}
                        ORDER BY COALESCE(g.sort_order, 0) ASC, g.id ASC
                        LIMIT :limit OFFSET :offset
                        """
                    ),
                    params,
                )
            ).mappings().all()
            binding_map = await self._load_binding_map(session, [int(row["id"]) for row in rows])
        return EvaluationPointGroupListVO(
            data=[self._to_group_vo(row, binding_map.get(int(row["id"]), []), include_rule_count=True) for row in rows],
            total=total,
            page=Page,
            page_size=PageSize,
        )

    async def ListAllGroups(self, IncludeDisabled: bool, WithRuleCount: bool) -> list[EvaluationPointGroupVO]:
        async with GetAsyncSession() as session:
            await self._ensure_ready(session)
            filters = ["g.deleted_at IS NULL", "(COALESCE(g.pid, 0) <> 0 OR g.document_type_id IS NOT NULL OR g.entry_module_id IS NOT NULL)"]
            if not IncludeDisabled:
                filters.append("g.is_enabled = TRUE")
            rows = (
                await session.execute(
                    text(
                        f"""
                        SELECT
                            g.id,
                            g.pid,
                            g.name,
                            g.code,
                            g.description,
                            g.document_type_id,
                            dt.name AS document_type_name,
                            COALESCE(g.entry_module_id, parent.entry_module_id, dt.entry_module_id) AS entry_module_id,
                            em.name AS entry_module_name,
                            g.sort_order,
                            g.is_enabled,
                            g.created_at,
                            g.updated_at,
                            COALESCE(bg.binding_count, 0) AS rule_count
                        FROM leaudit_evaluation_point_groups g
                        LEFT JOIN leaudit_document_types dt ON dt.id = g.document_type_id
                        LEFT JOIN leaudit_evaluation_point_groups parent ON parent.id = g.pid AND parent.deleted_at IS NULL
                        LEFT JOIN leaudit_entry_modules em ON em.id = COALESCE(g.entry_module_id, parent.entry_module_id, dt.entry_module_id)
                        LEFT JOIN (
                            SELECT group_id, COUNT(*)::int AS binding_count
                            FROM leaudit_rule_group_bindings
                            WHERE deleted_at IS NULL
                            GROUP BY group_id
                        ) bg ON bg.group_id = g.id
                        WHERE {' AND '.join(filters)}
                        ORDER BY COALESCE(g.sort_order, 0) ASC, g.id ASC
                        """
                    )
                )
            ).mappings().all()
            binding_map = await self._load_binding_map(session, [int(row["id"]) for row in rows])

        groups = [self._to_group_vo(row, binding_map.get(int(row["id"]), []), include_rule_count=WithRuleCount) for row in rows]
        by_parent: dict[int, list[EvaluationPointGroupVO]] = {}
        roots: list[EvaluationPointGroupVO] = []
        for group in groups:
            parent_id = self._normalize_pid(group.pid)
            if parent_id == 0:
                roots.append(group)
            else:
                by_parent.setdefault(parent_id, []).append(group)
        for group in groups:
            children = by_parent.get(group.id)
            if children:
                group.children = children
        return roots

    async def ListGroupsByDocumentTypes(
        self,
        DocumentTypeIds: list[int],
        IncludeDisabled: bool,
        WithRuleCount: bool,
    ) -> list[EvaluationPointGroupVO]:
        normalized_ids = sorted({int(item) for item in DocumentTypeIds if item})
        if not normalized_ids:
            return []
        roots = await self.ListAllGroups(IncludeDisabled=IncludeDisabled, WithRuleCount=WithRuleCount)
        result: list[EvaluationPointGroupVO] = []
        for root in roots:
            if root.document_type_id in normalized_ids:
                result.append(root)
                continue
            children = root.children or []
            if any(child.document_type_id in normalized_ids for child in children):
                result.append(root)
        return result

    async def GetGroup(self, GroupId: int, WithRuleCount: bool) -> EvaluationPointGroupVO:
        async with GetAsyncSession() as session:
            await self._ensure_ready(session)
            row = await self._get_group_row(session, GroupId)
            binding_map = await self._load_binding_map(session, [GroupId])
        return self._to_group_vo(row, binding_map.get(GroupId, []), include_rule_count=WithRuleCount)

    async def GetChildren(self, GroupId: int, IsEnabled: bool | None, Page: int, PageSize: int) -> EvaluationPointGroupListVO:
        await self.GetGroup(GroupId, WithRuleCount=False)
        return await self.ListGroups(Name=None, Code=None, IsEnabled=IsEnabled, Pid=GroupId, Page=Page, PageSize=PageSize)

    async def CreateGroup(self, Body: EvaluationPointGroupCreateDTO) -> EvaluationPointGroupVO:
        payload = self._normalize_create_payload(Body)
        async with GetAsyncSession() as session:
            await self._ensure_ready(session)
            await self._ensure_code_unique(session, payload["code"], None)
            parent = await self._ensure_parent_valid(session, payload["pid"])
            payload["entry_module_id"] = await self._ensure_entry_module_valid(
                session, payload["pid"], payload["entry_module_id"], parent
            )
            payload["document_type_id"] = await self._ensure_document_type_valid(
                session, payload["pid"], payload["document_type_id"], payload["entry_module_id"], None, parent
            )
            row = (
                await session.execute(
                    text(
                        """
                        INSERT INTO leaudit_evaluation_point_groups (
                            pid, code, name, description, document_type_id, entry_module_id, sort_order, is_enabled, created_at, updated_at
                        ) VALUES (
                            :pid, :code, :name, :description, :document_type_id, :entry_module_id, :sort_order, :is_enabled, NOW(), NOW()
                        )
                        RETURNING id
                        """
                    ),
                    payload,
                )
            ).mappings().one()
            await session.commit()
            group_id = int(row["id"])
        return await self.GetGroup(group_id, WithRuleCount=True)

    async def UpdateGroup(self, GroupId: int, Body: EvaluationPointGroupUpdateDTO) -> EvaluationPointGroupVO:
        async with GetAsyncSession() as session:
            await self._ensure_ready(session)
            current = await self._get_group_row(session, GroupId)
            provided_fields = set(getattr(Body, "model_fields_set", set()))
            next_pid = self._normalize_pid(Body.pid) if Body.pid is not None else self._normalize_pid(current["pid"])
            name = (Body.name.strip() if Body.name is not None else str(current.get("name") or "")).strip()
            code = (Body.code.strip() if Body.code is not None else str(current.get("code") or "")).strip()
            description = Body.description.strip() if Body.description is not None and Body.description else current.get("description")
            document_type_id = Body.document_type_id if "document_type_id" in provided_fields else current.get("document_type_id")
            entry_module_id = Body.entry_module_id if "entry_module_id" in provided_fields else current.get("entry_module_id")
            sort_order = Body.sort_order if Body.sort_order is not None else int(current.get("sort_order") or 0)
            is_enabled = Body.is_enabled if Body.is_enabled is not None else bool(current.get("is_enabled", True))

            if not name:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "分组名称不能为空")
            if not code:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "分组编码不能为空")

            await self._ensure_code_unique(session, code, GroupId)
            parent = await self._ensure_parent_valid(session, next_pid)
            entry_module_id = await self._ensure_entry_module_valid(session, next_pid, entry_module_id, parent)
            document_type_id = await self._ensure_document_type_valid(
                session, next_pid, document_type_id, entry_module_id, GroupId, parent
            )
            await session.execute(
                text(
                    """
                    UPDATE leaudit_evaluation_point_groups
                    SET pid = :pid,
                        code = :code,
                        name = :name,
                        description = :description,
                        document_type_id = :document_type_id,
                        entry_module_id = :entry_module_id,
                        sort_order = :sort_order,
                        is_enabled = :is_enabled,
                        updated_at = NOW()
                    WHERE id = :group_id
                    """
                ),
                {
                    "group_id": GroupId,
                    "pid": next_pid,
                    "code": code,
                    "name": name,
                    "description": description,
                    "document_type_id": document_type_id,
                    "entry_module_id": entry_module_id,
                    "sort_order": sort_order,
                    "is_enabled": is_enabled,
                },
            )
            await sync_doc_type_bindings_from_group(session, GroupId)
            await session.commit()
        return await self.GetGroup(GroupId, WithRuleCount=True)

    async def DeleteGroup(self, GroupId: int) -> EvaluationPointGroupDeleteVO:
        async with GetAsyncSession() as session:
            await self._ensure_ready(session)
            current = await self._get_group_row(session, GroupId)
            is_root = self._normalize_pid(current["pid"]) == 0
            if is_root:
                child_count = int(
                    (
                        await session.execute(
                            text(
                                "SELECT COUNT(*) FROM leaudit_evaluation_point_groups WHERE pid = :group_id AND deleted_at IS NULL"
                            ),
                            {"group_id": GroupId},
                        )
                    ).scalar_one()
                )
                if child_count > 0:
                    return EvaluationPointGroupDeleteVO(
                        success=False,
                        message="当前一级分组下仍存在二级分组，请先迁移或删除二级分组",
                        deleted_groups=0,
                        deleted_points=0,
                    )

            binding_count = int(
                (
                    await session.execute(
                        text(
                            "SELECT COUNT(*) FROM leaudit_rule_group_bindings WHERE group_id = :group_id AND deleted_at IS NULL"
                        ),
                        {"group_id": GroupId},
                    )
                ).scalar_one()
            )
            await session.execute(
                text(
                    "UPDATE leaudit_rule_group_bindings SET deleted_at = NOW(), updated_at = NOW() WHERE group_id = :group_id AND deleted_at IS NULL"
                ),
                {"group_id": GroupId},
            )
            await sync_doc_type_bindings_from_group(session, GroupId)
            result = await session.execute(
                text(
                    "UPDATE leaudit_evaluation_point_groups SET deleted_at = NOW(), updated_at = NOW() WHERE id = :group_id AND deleted_at IS NULL"
                ),
                {"group_id": GroupId},
            )
            await session.commit()
        deleted_groups = int(result.rowcount or 0)
        return EvaluationPointGroupDeleteVO(
            success=True,
            message="规则分组删除成功",
            deleted_count=deleted_groups,
            deleted_groups=deleted_groups,
            deleted_points=binding_count,
        )

    async def RebindGroup(self, GroupId: int, Body: EvaluationPointGroupRebindDTO) -> EvaluationPointGroupRebindVO:
        async with GetAsyncSession() as session:
            await self._ensure_ready(session)
            current = await self._get_group_row(session, GroupId)
            target = await self._get_group_row(session, Body.new_parent_id)
            if self._normalize_pid(current["pid"]) != 0 or self._normalize_pid(target["pid"]) != 0:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "仅支持一级分组之间迁移二级分组")
            if GroupId == Body.new_parent_id:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "迁移目标不能与原分组相同")
            result = await session.execute(
                text(
                    """
                    UPDATE leaudit_evaluation_point_groups
                    SET pid = :new_parent_id,
                        updated_at = NOW()
                    WHERE pid = :old_group_id AND deleted_at IS NULL
                    """
                ),
                {"old_group_id": GroupId, "new_parent_id": Body.new_parent_id},
            )
            await session.commit()
        moved = int(result.rowcount or 0)
        return EvaluationPointGroupRebindVO(success=True, message="二级分组迁移成功", rebind_count=moved, doc_types_updated=moved)

    async def BatchUpdateStatus(self, Body: EvaluationPointGroupBatchStatusDTO) -> EvaluationPointGroupBatchStatusVO:
        ids = sorted({int(item) for item in Body.ids if item})
        if not ids:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "请选择至少一个分组")
        async with GetAsyncSession() as session:
            await self._ensure_ready(session)
            result = await session.execute(
                text(
                    """
                    UPDATE leaudit_evaluation_point_groups
                    SET is_enabled = :is_enabled,
                        updated_at = NOW()
                    WHERE id IN :ids AND deleted_at IS NULL
                    """
                ).bindparams(bindparam("ids", expanding=True)),
                {"ids": ids, "is_enabled": Body.is_enabled},
            )
            await session.commit()
        updated_count = int(result.rowcount or 0)
        return EvaluationPointGroupBatchStatusVO(success=True, updated_count=updated_count, message=f"成功更新 {updated_count} 个分组状态")

    async def BatchDelete(self, Body: EvaluationPointGroupBatchDeleteDTO) -> EvaluationPointGroupBatchDeleteVO:
        ids = sorted({int(item) for item in Body.ids if item})
        if not ids:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "请选择至少一个分组")
        async with GetAsyncSession() as session:
            await self._ensure_ready(session)
            rows = (
                await session.execute(
                    text(
                        "SELECT id, pid, document_type_id FROM leaudit_evaluation_point_groups WHERE id IN :ids AND deleted_at IS NULL"
                    ).bindparams(bindparam("ids", expanding=True)),
                    {"ids": ids},
                )
            ).mappings().all()
            if len(rows) != len(ids):
                raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "部分分组不存在")
            if any(self._normalize_pid(row["pid"]) == 0 for row in rows):
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "批量删除仅支持二级分组")

            deleted_bindings = 0
            for row in rows:
                deleted_bindings += int(
                    (
                        await session.execute(
                            text(
                                "UPDATE leaudit_rule_group_bindings SET deleted_at = NOW(), updated_at = NOW() WHERE group_id = :group_id AND deleted_at IS NULL"
                            ),
                            {"group_id": int(row["id"])},
                        )
                    ).rowcount
                    or 0
                )
                await sync_doc_type_bindings_from_group(session, int(row["id"]))
            result = await session.execute(
                text(
                    "UPDATE leaudit_evaluation_point_groups SET deleted_at = NOW(), updated_at = NOW() WHERE id IN :ids AND deleted_at IS NULL"
                ).bindparams(bindparam("ids", expanding=True)),
                {"ids": ids},
            )
            await session.commit()
        deleted_groups = int(result.rowcount or 0)
        return EvaluationPointGroupBatchDeleteVO(success=True, deleted_groups=deleted_groups, deleted_points=deleted_bindings, message=f"成功删除 {deleted_groups} 个分组")

    async def CreateBinding(self, GroupId: int, Body: EvaluationPointGroupBindingCreateDTO) -> RuleGroupBindingVO:
        async with GetAsyncSession() as session:
            await self._ensure_ready(session)
            group = await self._get_group_row(session, GroupId)
            if self._normalize_pid(group["pid"]) == 0:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "一级分组不能直接绑定规则集，请先选择二级分组")
            await self._ensure_rule_set_valid(session, Body.rule_set_id)
            existing = (
                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": Body.rule_set_id},
                )
            ).mappings().first()
            if existing:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "该规则集已绑定到当前二级分组")
            row = (
                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, :is_active, :note, NOW(), NOW()
                        )
                        RETURNING id
                        """
                    ),
                    {
                        "group_id": GroupId,
                        "rule_set_id": Body.rule_set_id,
                        "priority": Body.priority,
                        "is_active": Body.is_active,
                        "note": Body.note.strip() if Body.note else None,
                    },
                )
            ).mappings().one()
            await sync_doc_type_bindings_from_group(session, GroupId)
            await session.commit()
            binding_id = int(row["id"])
            binding_row = await self._get_binding_row(session, binding_id)
            binding_vo = await self._build_binding_vo(binding_row)
        return binding_vo

    async def UpdateBinding(self, BindingId: int, Body: EvaluationPointGroupBindingUpdateDTO) -> RuleGroupBindingVO:
        async with GetAsyncSession() as session:
            await self._ensure_ready(session)
            current = await self._get_binding_row(session, BindingId)
            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": Body.priority if Body.priority is not None else int(current.get("priority") or 0),
                    "is_active": Body.is_active if Body.is_active is not None else bool(current.get("is_active", True)),
                    "note": Body.note.strip() if Body.note else (current.get("note") if Body.note is None else None),
                },
            )
            await sync_doc_type_bindings_from_group(session, int(current["group_id"]))
            await session.commit()
            binding_row = await self._get_binding_row(session, BindingId)
            binding_vo = await self._build_binding_vo(binding_row)
        return binding_vo

    async def DeleteBinding(self, BindingId: int) -> None:
        async with GetAsyncSession() as session:
            await self._ensure_ready(session)
            current = await self._get_binding_row(session, BindingId)
            await session.execute(
                text(
                    "UPDATE leaudit_rule_group_bindings SET deleted_at = NOW(), updated_at = NOW() WHERE id = :binding_id"
                ),
                {"binding_id": BindingId},
            )
            await sync_doc_type_bindings_from_group(session, int(current["group_id"]))
            await session.commit()

    async def GetRuleTemplate(self, GroupId: int) -> EvaluationPointGroupRuleTemplateVO:
        async with GetAsyncSession() as session:
            await self._ensure_ready(session)
            context = await self._load_rule_context(session, GroupId)
            template = self._build_rule_yaml_template(context)
            existing_binding_id = await self._get_group_binding_id_for_rule_type(session, GroupId, context["rule_type"])
        return EvaluationPointGroupRuleTemplateVO(
            context=EvaluationPointGroupRuleTemplateContextVO(
                group_id=context["group_id"],
                group_code=context["group_code"],
                group_name=context["group_name"],
                parent_group_id=context["parent_group_id"],
                parent_group_code=context["parent_group_code"],
                parent_group_name=context["parent_group_name"],
                document_type_id=context["document_type_id"],
                document_type_code=context["document_type_code"],
                document_type_name=context["document_type_name"],
                entry_module_id=context["entry_module_id"],
                entry_module_name=context["entry_module_name"],
            ),
            ruleType=context["rule_type"],
            ruleName=context["rule_name"],
            nextVersionNo=context["next_version_no"],
            ossPreviewKey=context["oss_preview_key"],
            yamlTemplate=template,
            existingRuleSetId=context["existing_rule_set_id"],
            existingBindingId=existing_binding_id,
        )

    async def CreateRuleDraft(self, GroupId: int, Body: EvaluationPointGroupRuleDraftCreateDTO) -> EvaluationPointGroupRuleDraftVO:
        async with GetAsyncSession() as session:
            await self._ensure_ready(session)
            context = await self._load_rule_context(session, GroupId)
            yaml_text = Body.yaml_text.strip()
            if not yaml_text:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "YAML 内容不能为空")

        created_version = await self.RuleService.CreateVersion(
            RuleType=context["rule_type"],
            YamlText=yaml_text,
            ChangeNote=Body.change_note.strip() if Body.change_note else None,
            EditorUserId=Body.editor_user_id,
        )

        async with GetAsyncSession() as session:
            await self._ensure_ready(session)
            rule_set_id = await self._get_rule_set_id_by_type(session, context["rule_type"])
            if rule_set_id is None:
                raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, "规则版本已创建，但未找到对应规则集")
            binding_id, auto_bound = await self._ensure_group_binding_for_rule_set(session, GroupId, rule_set_id)
            await sync_doc_type_bindings_from_group(session, GroupId)
            await session.commit()
            binding_row = await self._get_binding_row(session, binding_id)
            binding_vo = await self._build_binding_vo(binding_row)

        return EvaluationPointGroupRuleDraftVO(
            packId=GroupId,
            groupId=GroupId,
            ruleName=context["rule_name"],
            ruleType=context["rule_type"],
            ruleSetId=rule_set_id,
            ossKey=OssPathUtils.BuildRuleYamlKey(context["rule_type"], created_version.versionNo),
            version=created_version,
            binding=binding_vo,
            autoBound=auto_bound,
        )

    async def _ensure_ready(self, session) -> None:
        self._rule_set_meta_cache = None
        await ensure_rule_group_schema(session)
        await bootstrap_rule_groups(session)

    async def _get_group_row(self, session, group_id: int):
        row = (
            await session.execute(
                text(
                    """
                    SELECT
                        g.id,
                        g.pid,
                        g.name,
                        g.code,
                        g.description,
                        g.document_type_id,
                        dt.code AS document_type_code,
                        dt.name AS document_type_name,
                        parent.id AS root_group_id,
                        parent.name AS root_group_name,
                        parent.code AS root_group_code,
                        COALESCE(g.entry_module_id, parent.entry_module_id, dt.entry_module_id) AS entry_module_id,
                        em.name AS entry_module_name,
                        g.sort_order,
                        g.is_enabled,
                        g.created_at,
                        g.updated_at,
                        COALESCE(bg.binding_count, 0) AS rule_count
                    FROM leaudit_evaluation_point_groups g
                    LEFT JOIN leaudit_document_types dt ON dt.id = g.document_type_id
                    LEFT JOIN leaudit_evaluation_point_groups parent ON parent.id = g.pid AND parent.deleted_at IS NULL
                    LEFT JOIN leaudit_entry_modules em ON em.id = COALESCE(g.entry_module_id, parent.entry_module_id, dt.entry_module_id)
                    LEFT JOIN (
                        SELECT group_id, COUNT(*)::int AS binding_count
                        FROM leaudit_rule_group_bindings
                        WHERE deleted_at IS NULL
                        GROUP BY group_id
                    ) bg ON bg.group_id = g.id
                    WHERE g.id = :group_id AND g.deleted_at IS NULL
                    LIMIT 1
                    """
                ),
                {"group_id": group_id},
            )
        ).mappings().first()
        if not row:
            raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则分组不存在")
        return row

    async def _get_binding_row(self, session, binding_id: int):
        row = (
            await session.execute(
                text(
                    """
                    SELECT id, group_id, rule_set_id, rule_type_binding_id, priority, is_active, note, created_at, updated_at
                    FROM leaudit_rule_group_bindings
                    WHERE id = :binding_id AND deleted_at IS NULL
                    LIMIT 1
                    """
                ),
                {"binding_id": binding_id},
            )
        ).mappings().first()
        if not row:
            raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则组绑定不存在")
        return row

    async def _load_binding_map(self, session, group_ids: list[int]) -> dict[int, list[RuleGroupBindingVO]]:
        group_ids = [int(item) for item in group_ids if item]
        if not group_ids:
            return {}
        rows = (
            await session.execute(
                text(
                    """
                    SELECT id, group_id, rule_set_id, rule_type_binding_id, priority, is_active, note, created_at, updated_at
                    FROM leaudit_rule_group_bindings
                    WHERE group_id IN :group_ids AND deleted_at IS NULL
                    ORDER BY priority DESC, id ASC
                    """
                ).bindparams(bindparam("group_ids", expanding=True)),
                {"group_ids": group_ids},
            )
        ).mappings().all()
        result: dict[int, list[RuleGroupBindingVO]] = {}
        for row in rows:
            result.setdefault(int(row["group_id"]), []).append(await self._build_binding_vo(row))
        return result

    async def _build_binding_vo(self, row) -> RuleGroupBindingVO:
        rule_set_meta = await self._get_rule_set_meta(int(row["rule_set_id"]))
        return RuleGroupBindingVO(
            id=int(row["id"]),
            group_id=int(row["group_id"]),
            rule_set_id=int(row["rule_set_id"]),
            rule_type_binding_id=int(row["rule_type_binding_id"]) if row.get("rule_type_binding_id") else None,
            priority=int(row.get("priority") or 0),
            is_active=bool(row.get("is_active", True)),
            note=row.get("note"),
            rule_type=rule_set_meta.get("rule_type"),
            rule_name=rule_set_meta.get("rule_name"),
            current_version_id=rule_set_meta.get("current_version_id"),
            fallback_version_id=rule_set_meta.get("fallback_version_id"),
            has_usable_version=bool(rule_set_meta.get("has_usable_version", False)),
            usable_rule_count=int(rule_set_meta.get("usable_rule_count") or 0),
        )

    async def _get_rule_set_meta(self, rule_set_id: int) -> dict[str, Any]:
        if not getattr(self, "_rule_set_meta_cache", None):
            rule_sets = await self.RuleService.ListSets()
            self._rule_set_meta_cache = {
                int(item.id): {
                    "rule_type": item.ruleType,
                    "rule_name": item.ruleName,
                    "current_version_id": item.currentVersionId,
                    "fallback_version_id": item.fallbackVersionId,
                    "has_usable_version": item.hasUsableVersion,
                    "usable_rule_count": item.usableRuleCount,
                }
                for item in rule_sets
            }
        return self._rule_set_meta_cache.get(rule_set_id, {})

    async def _ensure_parent_valid(self, session, pid: int):
        if pid == 0:
            return None
        parent = await self._get_group_row(session, pid)
        if self._normalize_pid(parent["pid"]) != 0:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "上级分组必须是一级分组")
        return parent

    async def _ensure_entry_module_valid(
        self,
        session,
        pid: int,
        entry_module_id: int | None,
        parent: Any | None,
    ) -> int | None:
        if pid != 0:
            if parent is not None and parent.get("entry_module_id") is not None:
                return int(parent["entry_module_id"])
            return None if entry_module_id in (None, 0, "0", "") else int(entry_module_id)

        if entry_module_id in (None, 0, "0", ""):
            return None

        exists = (
            await session.execute(
                text("SELECT id FROM leaudit_entry_modules WHERE id = :entry_module_id AND deleted_at IS NULL LIMIT 1"),
                {"entry_module_id": int(entry_module_id)},
            )
        ).scalar_one_or_none()
        if exists is None:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "关联入口模块不存在")
        return int(entry_module_id)

    async def _ensure_document_type_valid(
        self,
        session,
        pid: int,
        document_type_id: int | None,
        entry_module_id: int | None,
        group_id: int | None,
        parent: Any | None,
    ) -> int | None:
        if pid == 0:
            if document_type_id is None and entry_module_id is None:
                return None
        else:
            if parent is None:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "二级分组必须挂到一级分组下")
            if parent.get("document_type_id") is not None:
                parent_doc_type_id = int(parent["document_type_id"])
                if document_type_id is None:
                    document_type_id = parent_doc_type_id
                elif int(document_type_id) != parent_doc_type_id:
                    raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "二级分组的文档类型必须与所属一级分组一致")
            elif document_type_id is None:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "二级分组必须明确绑定具体文档类型")

        if document_type_id is None:
            return None
        exists = (
            await session.execute(
                text(
                    "SELECT id FROM leaudit_document_types WHERE id = :doc_type_id AND deleted_at IS NULL LIMIT 1"
                ),
                {"doc_type_id": int(document_type_id)},
            )
        ).scalar_one_or_none()
        if exists is None:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "关联文档类型不存在")
        if pid == 0:
            duplicated_root = (
                await session.execute(
                    text(
                        """
                        SELECT id
                        FROM leaudit_evaluation_point_groups
                        WHERE deleted_at IS NULL
                          AND COALESCE(pid, 0) = 0
                          AND document_type_id = :doc_type_id
                          AND (:group_id IS NULL OR id <> :group_id)
                        LIMIT 1
                        """
                    ),
                    {"doc_type_id": int(document_type_id), "group_id": group_id},
                )
            ).scalar_one_or_none()
            if duplicated_root is not None:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "该文档类型已存在一级分组")
        return int(document_type_id)

    async def _ensure_code_unique(self, session, code: str, group_id: int | None) -> None:
        sql = "SELECT id FROM leaudit_evaluation_point_groups WHERE LOWER(code) = LOWER(:code) AND deleted_at IS NULL"
        params: dict[str, Any] = {"code": code}
        if group_id is not None:
            sql += " AND id <> :group_id"
            params["group_id"] = group_id
        duplicated = (await session.execute(text(sql + " LIMIT 1"), params)).scalar_one_or_none()
        if duplicated is not None:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "分组编码已存在")

    async def _ensure_rule_set_valid(self, session, rule_set_id: int) -> None:
        exists = (
            await session.execute(
                text("SELECT id FROM leaudit_rule_sets WHERE id = :rule_set_id AND deleted_at IS NULL LIMIT 1"),
                {"rule_set_id": rule_set_id},
            )
        ).scalar_one_or_none()
        if exists is None:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "规则集不存在")

    async def _load_rule_context(self, session, group_id: int) -> dict[str, Any]:
        row = await self._get_group_row(session, group_id)
        if self._normalize_pid(row["pid"]) == 0:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "请先选择二级分组，再创建规则 YAML")

        rule_type = self._resolve_rule_type(row)
        if not rule_type:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前二级分组缺少可推导的规则类型编码，请先补充分组编码或文档类型编码")

        rule_name = str(row.get("document_type_name") or row.get("name") or "").strip()
        if not rule_name:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前二级分组缺少规则名称，请先补充分组名称")

        next_version_no = await self._get_next_rule_version_no(session, rule_type)
        return {
            "group_id": int(row["id"]),
            "group_code": str(row.get("code") or "").strip(),
            "group_name": str(row.get("name") or ""),
            "parent_group_id": int(row.get("root_group_id") or 0),
            "parent_group_code": str(row.get("root_group_code") or "").strip(),
            "parent_group_name": str(row.get("root_group_name") or row.get("entry_module_name") or ""),
            "document_type_id": int(row["document_type_id"]) if row.get("document_type_id") is not None else None,
            "document_type_code": str(row.get("document_type_code") or "").strip() or None,
            "document_type_name": str(row.get("document_type_name") or "").strip() or None,
            "entry_module_id": int(row["entry_module_id"]) if row.get("entry_module_id") is not None else None,
            "entry_module_name": str(row.get("entry_module_name") or "").strip() or None,
            "rule_type": rule_type,
            "rule_name": rule_name,
            "next_version_no": next_version_no,
            "oss_preview_key": OssPathUtils.BuildRuleYamlKey(rule_type, next_version_no),
            "existing_rule_set_id": await self._get_rule_set_id_by_type(session, rule_type),
        }

    def _resolve_rule_type(self, row: Any) -> str:
        doc_type_code = str(row.get("document_type_code") or "").strip()
        if doc_type_code:
            return self._normalize_rule_code(doc_type_code)
        child_code = str(row.get("code") or "").strip()
        root_code = str(row.get("root_group_code") or "").strip()
        if child_code and root_code and child_code != root_code and not child_code.startswith(f"{root_code}."):
            return self._normalize_rule_code(f"{root_code}.{child_code}")
        if child_code:
            return self._normalize_rule_code(child_code)
        group_name = str(row.get("name") or "").strip()
        if root_code and group_name:
            return self._normalize_rule_code(f"{root_code}.{group_name}")
        if group_name:
            return self._normalize_rule_code(group_name)
        return ""

    def _normalize_rule_code(self, value: str) -> str:
        normalized = ".".join(segment.strip() for segment in value.strip().split(".") if segment.strip())
        if not normalized:
            return ""
        return normalized.replace("/", "_").replace("\\", "_").replace(" ", "_")

    async def _get_next_rule_version_no(self, session, rule_type: str) -> str:
        row = (
            await session.execute(
                text(
                    """
                    SELECT COALESCE(MAX(rv.version_seq), 0) AS max_seq
                    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
                    """
                ),
                {"rule_type": rule_type},
            )
        ).mappings().first()
        next_seq = int((row or {}).get("max_seq") or 0) + 1
        return f"v{next_seq}"

    def _build_rule_yaml_template(self, context: dict[str, Any]) -> str:
        keywords = [
            item
            for item in dict.fromkeys(
                [
                    context.get("parent_group_name"),
                    context.get("group_name"),
                    context.get("document_type_name"),
                    context.get("entry_module_name"),
                ]
            )
            if item
        ]
        keyword_lines = "\n".join(f"  - {item}" for item in keywords) or "  - 待补充"
        description = (
            f"{context['rule_name']} 规则模板。"
            f" 入口模块：{context.get('entry_module_name') or '未配置'}；"
            f" 一级分组：{context.get('parent_group_name') or '未配置'}；"
            f" 二级分组：{context.get('group_name') or '未配置'}。"
        )
        parent_type = context.get("parent_group_name") or context.get("group_name") or context["rule_type"]
        return (
            "metadata:\n"
            f"  type_id: {context['rule_type']}\n"
            f"  name: {context['rule_name']}\n"
            f"  version: {context['next_version_no']}\n"
            f"  last_updated: '{datetime.now().date().isoformat()}'\n"
            f"  parent: {parent_type}\n"
            "  classification_keywords:\n"
            f"{keyword_lines}\n"
            "  description: >\n"
            f"    {description}\n\n"
            "sub_documents: []\n"
            "rules: []\n"
        )

    async def _get_group_binding_id_for_rule_type(self, session, group_id: int, rule_type: str) -> int | None:
        rule_set_id = await self._get_rule_set_id_by_type(session, rule_type)
        if rule_set_id is None:
            return None
        row = (
            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": group_id, "rule_set_id": rule_set_id},
            )
        ).mappings().first()
        return int(row["id"]) if row else None

    async def _get_rule_set_id_by_type(self, session, rule_type: str) -> int | None:
        row = (
            await session.execute(
                text("SELECT id FROM leaudit_rule_sets WHERE rule_type = :rule_type AND deleted_at IS NULL LIMIT 1"),
                {"rule_type": rule_type},
            )
        ).mappings().first()
        return int(row["id"]) if row else None

    async def _ensure_group_binding_for_rule_set(self, session, group_id: int, rule_set_id: int) -> tuple[int, bool]:
        existing = (
            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": group_id, "rule_set_id": rule_set_id},
            )
        ).mappings().first()
        if existing:
            return int(existing["id"]), False

        row = (
            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, 100, TRUE, :note, NOW(), NOW()
                    )
                    RETURNING id
                    """
                ),
                {
                    "group_id": group_id,
                    "rule_set_id": rule_set_id,
                    "note": "由二级分组新建规则 YAML 时自动补绑",
                },
            )
        ).mappings().one()
        return int(row["id"]), True

    def _normalize_create_payload(self, body: EvaluationPointGroupCreateDTO) -> dict[str, Any]:
        name = body.name.strip()
        code = body.code.strip()
        if not name:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "分组名称不能为空")
        if not code:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "分组编码不能为空")
        return {
            "name": name,
            "code": code,
            "pid": self._normalize_pid(body.pid),
            "description": body.description.strip() if body.description else None,
            "document_type_id": body.document_type_id,
            "entry_module_id": body.entry_module_id,
            "sort_order": int(body.sort_order or 0),
            "is_enabled": body.is_enabled,
        }

    def _to_group_vo(self, row, bindings: list[RuleGroupBindingVO], include_rule_count: bool) -> EvaluationPointGroupVO:
        return EvaluationPointGroupVO(
            id=int(row["id"]),
            pid=self._normalize_pid(row.get("pid")),
            name=str(row.get("name") or ""),
            code=str(row.get("code") or ""),
            description=row.get("description"),
            document_type_id=int(row["document_type_id"]) if row.get("document_type_id") is not None else None,
            document_type_name=row.get("document_type_name"),
            entry_module_id=int(row["entry_module_id"]) if row.get("entry_module_id") is not None else None,
            entry_module_name=row.get("entry_module_name"),
            sort_order=int(row.get("sort_order") or 0),
            is_enabled=bool(row.get("is_enabled", True)),
            created_at=self._to_iso(row.get("created_at")),
            updated_at=self._to_iso(row.get("updated_at")),
            rule_count=len(bindings) if include_rule_count else None,
            bindings=bindings,
            children=None,
        )

    def _normalize_pid(self, value: Any) -> int:
        if value in (None, "", "0", 0):
            return 0
        return int(value)

    def _to_iso(self, value: Any) -> str | None:
        if value is None:
            return None
        if isinstance(value, datetime):
            return value.isoformat()
        return str(value)
