"""交叉评查服务实现（第一阶段骨架）。"""

from __future__ import annotations

import io
import logging
from datetime import datetime
from math import floor
from urllib.parse import quote
from xml.sax.saxutils import escape
from zipfile import ZIP_DEFLATED, ZipFile

from sqlalchemy import bindparam, text

logger = logging.getLogger(__name__)

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.crossReviewDto import (
    CrossReviewProposalCreateDTO,
    CrossReviewProposalVoteDTO,
    CrossReviewTaskCreateDTO,
    CrossReviewTaskDocumentQueryDTO,
    CrossReviewTaskQueryDTO,
)
from fastapi_modules.fastapi_leaudit.domian.vo.crossReviewVo import (
    CrossReviewTaskDocumentAppendVO,
    CrossReviewTaskHistoryVersionVO,
    CrossReviewPendingProposalVO,
    CrossReviewPendingVotesVO,
    CrossReviewPermissionVO,
    CrossReviewProposalCancelVO,
    CrossReviewProposalCreateVO,
    CrossReviewProposalItemVO,
    CrossReviewProposalPageVO,
    CrossReviewProposalVoteItemVO,
    CrossReviewProposalVoteVO,
    CrossReviewTaskCompleteVO,
    CrossReviewTaskCreateVO,
    CrossReviewTaskDocumentPageVO,
    CrossReviewTaskDocumentUploadVO,
    CrossReviewTaskDocumentVO,
    CrossReviewTaskItemVO,
    CrossReviewTaskPageVO,
    CrossReviewTaskProgressVO,
    CrossReviewTaskTenantVO,
)
from fastapi_modules.fastapi_leaudit.services.crossReviewService import ICrossReviewService
from fastapi_modules.fastapi_leaudit.services.documentService import IDocumentService
from fastapi_modules.fastapi_leaudit.services.auditService import IAuditService
from fastapi_modules.fastapi_leaudit.services.impl.auditServiceImpl import AuditServiceImpl
from fastapi_modules.fastapi_leaudit.services.impl.documentServiceImpl import DocumentServiceImpl
from fastapi_modules.fastapi_leaudit.services.impl.ssoUserCompat import SsoUserCompat
from fastapi_modules.fastapi_leaudit.services.impl.tenantResolver import TenantResolver


class CrossReviewServiceImpl(ICrossReviewService):
    """交叉评查服务实现。"""

    _PROPOSAL_EXPORT_HEADERS: tuple[str, ...] = (
        "任务名称",
        "文档名称",
        "意见分类",
        "评查点名称",
        "问题依据/命中内容",
        "原问题说明",
        "评查意见",
        "分数调整",
        "提案状态",
        "发起人",
        "提出时间",
        "同意人数",
        "同意人员",
        "不同意人数",
        "不同意人员",
        "未投人数",
        "未投人员",
    )

    _VOTE_EXPORT_HEADERS: tuple[str, ...] = (
        "任务名称",
        "文档名称",
        "意见分类",
        "评查点名称",
        "发起人",
        "投票人",
        "投票状态",
        "投票时间",
    )

    _SCHEMA_BOOTSTRAP_STATEMENTS: tuple[str, ...] = (
        """
        CREATE TABLE IF NOT EXISTS leaudit_cross_review_tasks (
            id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
            task_name VARCHAR(255) NOT NULL,
            task_type VARCHAR(32) NOT NULL,
            doc_type_id BIGINT,
            doc_type_code VARCHAR(64),
            assigner_id BIGINT NOT NULL,
            status VARCHAR(32) NOT NULL DEFAULT 'in_progress',
            create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
            update_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
            delete_time TIMESTAMPTZ
        )
        """,
        "CREATE INDEX IF NOT EXISTS idx_lcr_tasks_assigner_id ON leaudit_cross_review_tasks (assigner_id)",
        "CREATE INDEX IF NOT EXISTS idx_lcr_tasks_status ON leaudit_cross_review_tasks (status)",
        "CREATE INDEX IF NOT EXISTS idx_lcr_tasks_doc_type_id ON leaudit_cross_review_tasks (doc_type_id)",
        """
        CREATE TABLE IF NOT EXISTS leaudit_cross_review_task_members (
            id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
            task_id BIGINT NOT NULL,
            user_id BIGINT NOT NULL,
            member_role VARCHAR(32) NOT NULL DEFAULT 'participant',
            create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
            update_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
            delete_time TIMESTAMPTZ
        )
        """,
        "CREATE INDEX IF NOT EXISTS idx_lcr_task_members_task_id ON leaudit_cross_review_task_members (task_id)",
        "CREATE INDEX IF NOT EXISTS idx_lcr_task_members_user_id ON leaudit_cross_review_task_members (user_id)",
        "CREATE INDEX IF NOT EXISTS idx_lcr_task_members_role ON leaudit_cross_review_task_members (member_role)",
        """
        CREATE UNIQUE INDEX IF NOT EXISTS uq_lcr_task_members_task_user_active
            ON leaudit_cross_review_task_members (task_id, user_id)
            WHERE delete_time IS NULL
        """,
        """
        CREATE TABLE IF NOT EXISTS leaudit_cross_review_task_documents (
            id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
            task_id BIGINT NOT NULL,
            document_id BIGINT NOT NULL,
            audit_status INTEGER NOT NULL DEFAULT 0,
            create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
            update_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
            delete_time TIMESTAMPTZ
        )
        """,
        "CREATE INDEX IF NOT EXISTS idx_lcr_task_documents_task_id ON leaudit_cross_review_task_documents (task_id)",
        "CREATE INDEX IF NOT EXISTS idx_lcr_task_documents_document_id ON leaudit_cross_review_task_documents (document_id)",
        "CREATE INDEX IF NOT EXISTS idx_lcr_task_documents_task_status ON leaudit_cross_review_task_documents (task_id, audit_status)",
        """
        CREATE UNIQUE INDEX IF NOT EXISTS uq_lcr_task_documents_task_document_active
            ON leaudit_cross_review_task_documents (task_id, document_id)
            WHERE delete_time IS NULL
        """,
        """
        CREATE TABLE IF NOT EXISTS leaudit_cross_review_proposals (
            id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
            task_id BIGINT NOT NULL,
            document_id BIGINT NOT NULL,
            proposal_type VARCHAR(32) NOT NULL DEFAULT 'review_point',
            rule_result_id BIGINT,
            proposer_id BIGINT NOT NULL,
            evaluation_point_name VARCHAR(255),
            extraction_result_text TEXT,
            proposed_score_delta NUMERIC(10, 2) NOT NULL,
            reason TEXT NOT NULL,
            status VARCHAR(32) NOT NULL DEFAULT 'pending',
            create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
            update_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
            delete_time TIMESTAMPTZ
        )
        """,
        "CREATE INDEX IF NOT EXISTS idx_lcr_proposals_task_id ON leaudit_cross_review_proposals (task_id)",
        "CREATE INDEX IF NOT EXISTS idx_lcr_proposals_document_id ON leaudit_cross_review_proposals (document_id)",
        "CREATE INDEX IF NOT EXISTS idx_lcr_proposals_rule_result_id ON leaudit_cross_review_proposals (rule_result_id)",
        "CREATE INDEX IF NOT EXISTS idx_lcr_proposals_proposer_id ON leaudit_cross_review_proposals (proposer_id)",
        "CREATE INDEX IF NOT EXISTS idx_lcr_proposals_status ON leaudit_cross_review_proposals (status)",
        """
        CREATE TABLE IF NOT EXISTS leaudit_cross_review_votes (
            id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
            proposal_id BIGINT NOT NULL,
            voter_id BIGINT NOT NULL,
            vote_type VARCHAR(16) NOT NULL,
            create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
            update_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
            delete_time TIMESTAMPTZ
        )
        """,
        "CREATE INDEX IF NOT EXISTS idx_lcr_votes_proposal_id ON leaudit_cross_review_votes (proposal_id)",
        "CREATE INDEX IF NOT EXISTS idx_lcr_votes_voter_id ON leaudit_cross_review_votes (voter_id)",
        """
        CREATE UNIQUE INDEX IF NOT EXISTS uq_lcr_votes_proposal_voter_active
            ON leaudit_cross_review_votes (proposal_id, voter_id)
            WHERE delete_time IS NULL
        """,
    )

    def __init__(self):
        self.DocumentService: IDocumentService = DocumentServiceImpl()
        self.AuditService: IAuditService = AuditServiceImpl()
        self.TenantResolver = TenantResolver()

    async def CreateTask(self, CurrentUserId: int, Body: CrossReviewTaskCreateDTO) -> CrossReviewTaskCreateVO:
        """创建交叉评查任务。"""
        async with GetAsyncSession() as session:
            await self._ensure_tables_ready(session)

            memberUserIds = self._unique_int_list(Body.memberUserIds + [CurrentUserId])
            principalUserIds = self._unique_int_list(Body.principalUserIds)
            documentIds = self._unique_int_list(Body.documentIds)
            await self._assert_task_scope_inputs(session, CurrentUserId, memberUserIds, documentIds)

            await self._reset_transaction_for_write(session)
            async with session.begin():
                taskRow = (
                    await session.execute(
                        text(
                            """
                            INSERT INTO leaudit_cross_review_tasks
                                (task_name, task_type, doc_type_id, doc_type_code, assigner_id, status)
                            VALUES
                                (:task_name, :task_type, :doc_type_id, :doc_type_code, :assigner_id, 'in_progress')
                            RETURNING id, task_name
                            """
                        ),
                        {
                            "task_name": Body.taskName.strip(),
                            "task_type": Body.taskType,
                            "doc_type_id": Body.docTypeId,
                            "doc_type_code": Body.docTypeCode,
                            "assigner_id": CurrentUserId,
                        },
                    )
                ).mappings().first()

                if not taskRow:
                    raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, "创建交叉评查任务失败")

                taskId = int(taskRow["id"])

                for userId in memberUserIds:
                    await session.execute(
                        text(
                            """
                            INSERT INTO leaudit_cross_review_task_members
                                (task_id, user_id, member_role)
                            VALUES
                                (:task_id, :user_id, :member_role)
                            """
                        ),
                        {
                            "task_id": taskId,
                            "user_id": userId,
                            "member_role": "principal" if userId in principalUserIds else "participant",
                        },
                    )

                for documentId in documentIds:
                    await session.execute(
                        text(
                            """
                            INSERT INTO leaudit_cross_review_task_documents
                                (task_id, document_id, audit_status)
                            VALUES
                                (:task_id, :document_id, 0)
                            """
                        ),
                        {"task_id": taskId, "document_id": documentId},
                    )
                    await session.execute(
                        text(
                            "UPDATE leaudit_documents SET review_scope = 'cross_review' WHERE id = :id"
                        ),
                        {"id": documentId},
                    )

            for documentId in documentIds:
                try:
                    await self.AuditService.Run(
                        DocumentId=documentId,
                        Force=False,
                        Speed="normal",
                        TriggerUserId=CurrentUserId,
                    )
                except Exception as exc:
                    logger.warning(
                        "交叉评查任务创建时触发评查失败: task_id=%s document_id=%s error=%s",
                        taskId, documentId, exc,
                    )

            return CrossReviewTaskCreateVO(
                taskId=taskId,
                taskName=str(taskRow["task_name"]),
                memberCount=len(memberUserIds),
                documentCount=len(documentIds),
            )

    async def GetUserTasks(self, CurrentUserId: int, Body: CrossReviewTaskQueryDTO) -> CrossReviewTaskPageVO:
        """查询当前用户参与的交叉评查任务。"""
        async with GetAsyncSession() as session:
            await self._ensure_tables_ready(session)
            offset = (Body.page - 1) * Body.pageSize
            params: dict[str, object] = {
                "current_user_id": CurrentUserId,
                "limit": Body.pageSize,
                "offset": offset,
            }
            whereClauses = [
                "tm.user_id = :current_user_id",
                "tm.delete_time IS NULL",
                "t.delete_time IS NULL",
            ]
            if Body.keyword:
                whereClauses.append("t.task_name ILIKE :keyword")
                params["keyword"] = f"%{Body.keyword.strip()}%"
            if Body.status:
                whereClauses.append("t.status = :status")
                params["status"] = Body.status
            if Body.taskType:
                whereClauses.append("t.task_type = :task_type")
                params["task_type"] = Body.taskType
            if Body.docTypeCode:
                whereClauses.append("t.doc_type_code = :doc_type_code")
                params["doc_type_code"] = Body.docTypeCode

            whereSql = " AND ".join(whereClauses)

            total = int(
                (
                    await session.scalar(
                        text(
                            f"""
                            SELECT COUNT(DISTINCT t.id)
                            FROM leaudit_cross_review_tasks t
                            JOIN leaudit_cross_review_task_members tm
                              ON tm.task_id = t.id
                            WHERE {whereSql}
                            """
                        ),
                        params,
                    )
                )
                or 0
            )

            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",
            )
            rows = (
                await session.execute(
                    text(
                        f"""
                        WITH doc_stats AS (
                            SELECT
                                td.task_id,
                                COUNT(*)::int AS total_documents,
                                COUNT(*) FILTER (WHERE td.audit_status = 1)::int AS completed_documents
                            FROM leaudit_cross_review_task_documents td
                            WHERE td.delete_time IS NULL
                            GROUP BY td.task_id
                        ),
                        task_regions AS (
                            SELECT
                                tm.task_id,
                                ARRAY_AGG(
                                    DISTINCT COALESCE(NULLIF({tenant_name_expr}, ''), NULLIF(u.area, ''))
                                    ORDER BY COALESCE(NULLIF({tenant_name_expr}, ''), NULLIF(u.area, ''))
                                ) FILTER (
                                    WHERE COALESCE(NULLIF({tenant_name_expr}, ''), NULLIF(u.area, '')) IS NOT NULL
                                ) AS evaluation_regions
                            FROM leaudit_cross_review_task_members tm
                            JOIN sso_users u ON u.id = tm.user_id
                            WHERE tm.delete_time IS NULL
                            GROUP BY tm.task_id
                        ),
                        task_tenants AS (
                            SELECT
                                tm.task_id,
                                jsonb_agg(
                                    DISTINCT jsonb_build_object(
                                        'tenantCode',
                                        COALESCE(NULLIF({tenant_code_expr}, ''), ''),
                                        'tenantName',
                                        COALESCE(NULLIF({tenant_name_expr}, ''), NULLIF(u.area, ''), '')
                                    )
                                ) FILTER (
                                    WHERE COALESCE(NULLIF({tenant_name_expr}, ''), NULLIF(u.area, '')) IS NOT NULL
                                ) AS evaluation_tenants
                            FROM leaudit_cross_review_task_members tm
                            JOIN sso_users u ON u.id = tm.user_id
                            WHERE tm.delete_time IS NULL
                            GROUP BY tm.task_id
                        )
                        SELECT
                            t.id AS task_id,
                            t.task_name,
                            t.task_type,
                            t.doc_type_id,
                            t.doc_type_code,
                            t.status,
                            t.create_time,
                            COALESCE(ds.total_documents, 0) AS total_documents,
                            COALESCE(ds.completed_documents, 0) AS completed_documents,
                            COALESCE(tt.evaluation_tenants, '[]'::jsonb) AS evaluation_tenants,
                            COALESCE(tr.evaluation_regions, ARRAY[]::varchar[]) AS evaluation_regions
                        FROM leaudit_cross_review_tasks t
                        JOIN leaudit_cross_review_task_members tm
                          ON tm.task_id = t.id
                        LEFT JOIN doc_stats ds
                          ON ds.task_id = t.id
                        LEFT JOIN task_tenants tt
                          ON tt.task_id = t.id
                        LEFT JOIN task_regions tr
                          ON tr.task_id = t.id
                        WHERE {whereSql}
                        GROUP BY
                            t.id, t.task_name, t.task_type, t.doc_type_id, t.doc_type_code,
                            t.status, t.create_time, ds.total_documents, ds.completed_documents, tt.evaluation_tenants,
                            tr.evaluation_regions
                        ORDER BY t.create_time DESC, t.id DESC
                        LIMIT :limit OFFSET :offset
                        """
                    ),
                    params,
                )
            ).mappings().all()

            items = []
            for row in rows:
                totalDocuments = int(row["total_documents"] or 0)
                completedDocuments = int(row["completed_documents"] or 0)
                progress = round((completedDocuments / totalDocuments * 100) if totalDocuments > 0 else 0, 2)
                evaluationTenants = self._parse_task_tenants(row.get("evaluation_tenants"))
                rawRegions = row.get("evaluation_regions")
                if rawRegions is None:
                    evaluationRegion: list[str] = []
                elif isinstance(rawRegions, list):
                    evaluationRegion = [str(r) for r in rawRegions]
                else:
                    evaluationRegion = [str(rawRegions)]
                if not evaluationRegion:
                    evaluationRegion = [tenant.tenantName for tenant in evaluationTenants if tenant.tenantName]
                items.append(
                    CrossReviewTaskItemVO(
                        taskId=int(row["task_id"]),
                        taskName=str(row["task_name"]),
                        taskType=str(row["task_type"]),
                        docTypeId=self._to_int(row.get("doc_type_id")),
                        docTypeCode=row.get("doc_type_code"),
                        status=str(row["status"]),
                        progress=progress,
                        totalDocuments=totalDocuments,
                        completedDocuments=completedDocuments,
                        createdAt=row.get("create_time"),
                        evaluationTenants=evaluationTenants,
                        evaluationRegion=evaluationRegion,
                    )
                )

            return CrossReviewTaskPageVO(total=total, page=Body.page, pageSize=Body.pageSize, items=items)

    async def GetTaskProgress(self, CurrentUserId: int, TaskId: int) -> CrossReviewTaskProgressVO:
        """查询任务进度。"""
        async with GetAsyncSession() as session:
            await self._ensure_tables_ready(session)
            await self._ensure_task_member(session, TaskId, CurrentUserId)

            row = (
                await session.execute(
                    text(
                        """
                        SELECT
                            COUNT(*)::int AS total_documents,
                            COUNT(*) FILTER (WHERE audit_status = 1)::int AS completed_documents
                        FROM leaudit_cross_review_task_documents
                        WHERE task_id = :task_id
                          AND delete_time IS NULL
                        """
                    ),
                    {"task_id": TaskId},
                )
            ).mappings().first()

            totalDocuments = int((row or {}).get("total_documents") or 0)
            completedDocuments = int((row or {}).get("completed_documents") or 0)
            progress = round((completedDocuments / totalDocuments * 100) if totalDocuments > 0 else 0, 2)
            return CrossReviewTaskProgressVO(
                taskId=TaskId,
                totalDocuments=totalDocuments,
                completedDocuments=completedDocuments,
                progress=progress,
            )

    async def GetTaskDocuments(
        self,
        CurrentUserId: int,
        TaskId: int,
        Body: CrossReviewTaskDocumentQueryDTO,
    ) -> CrossReviewTaskDocumentPageVO:
        """查询任务文档列表。"""
        async with GetAsyncSession() as session:
            await self._ensure_tables_ready(session)
            await self._ensure_task_member(session, TaskId, CurrentUserId)

            params: dict[str, object] = {
                "task_id": TaskId,
                "limit": Body.pageSize,
                "offset": (Body.page - 1) * Body.pageSize,
            }
            baseWhereClauses = [
                "td.task_id = :task_id",
                "td.delete_time IS NULL",
                "d.deleted_at IS NULL",
            ]
            if Body.keyword:
                baseWhereClauses.append("(d.normalized_name ILIKE :keyword OR CAST(d.biz_document_id AS TEXT) ILIKE :keyword)")
                params["keyword"] = f"%{Body.keyword.strip()}%"
            baseWhereSql = " AND ".join(baseWhereClauses)
            latestWhereSql = f"{baseWhereSql} AND COALESCE(d.is_latest_version, false) = true"

            total = int(
                (
                    await session.scalar(
                        text(
                            f"""
                            SELECT COUNT(*)
                            FROM leaudit_cross_review_task_documents td
                            JOIN leaudit_documents d
                              ON d.id = td.document_id
                            WHERE {latestWhereSql}
                            """
                        ),
                        params,
                    )
                )
                or 0
            )

            rows = (
                await session.execute(
                    text(
                        f"""
                        SELECT
                            d.id AS document_id,
                            COALESCE(d.normalized_name, '') AS name,
                            CAST(d.biz_document_id AS TEXT) AS document_number,
                            d.type_id,
                            COALESCE(
                                CASE
                                    WHEN LOWER(COALESCE(ar.status, '')) IN ('pending', 'queued', 'running', 'retrying')
                                        THEN ar.status
                                    ELSE d.processing_status
                                END,
                                d.processing_status,
                                'waiting'
                            ) AS processing_status,
                            d.version_no,
                            d.is_latest_version,
                            COALESCE(NULLIF(d.version_group_key, ''), CONCAT('root:', COALESCE(d.root_version_id, d.id)::text)) AS version_group_key,
                            COALESCE(vc.total_versions, 1)::int AS total_versions,
                            d.created_at,
                            td.audit_status,
                            COALESCE(dt.name, '') AS type_name,
                            COALESCE(df.file_size, 0) AS file_size,
                            COALESCE(df.file_path, '') AS path,
                            df.file_upload_time AS upload_time,
                            COALESCE(df.file_ext, '') AS file_ext,
                            COALESCE(es.total_evaluation_points, 0) AS total_evaluation_points,
                            COALESCE(es.pass_count, 0) AS pass_count,
                            COALESCE(es.warning_count, 0) AS warning_count,
                            COALESCE(es.error_count, 0) AS error_count,
                            COALESCE(es.manual_count, 0) AS manual_count,
                            COALESCE(es.issue_count, 0) AS issue_count,
                            COALESCE(es.warning_messages, ARRAY[]::text[]) AS warning_messages,
                            COALESCE(es.error_messages, ARRAY[]::text[]) AS error_messages,
                            COALESCE(es.issue_messages, ARRAY[]::text[]) AS issue_messages,
                            COALESCE(es.manual_messages, ARRAY[]::text[]) AS manual_messages,
                            COALESCE(es.final_score, 0) AS final_score,
                            COALESCE(es.full_score, 0) AS full_score,
                            COALESCE(es.score_summary, '') AS score_summary,
                            COALESCE(es.score_percent, 0) AS score_percent
                        FROM leaudit_cross_review_task_documents td
                        JOIN leaudit_documents d
                          ON d.id = td.document_id
                        LEFT JOIN leaudit_audit_runs ar
                          ON ar.id = d.current_run_id
                        LEFT JOIN leaudit_document_types dt
                          ON dt.id = d.type_id
                        LEFT JOIN LATERAL (
                            SELECT file_size, local_path AS file_path, created_at AS file_upload_time,
                                   COALESCE(file_ext, '') AS file_ext
                            FROM leaudit_document_files
                            WHERE document_id = d.id
                            ORDER BY id ASC
                            LIMIT 1
                        ) df ON TRUE
                        LEFT JOIN (
                            SELECT
                                COALESCE(NULLIF(d2.version_group_key, ''), CONCAT('root:', COALESCE(d2.root_version_id, d2.id)::text)) AS version_group_key,
                                COUNT(*) AS total_versions
                            FROM leaudit_documents d2
                            JOIN leaudit_cross_review_task_documents td2
                              ON td2.document_id = d2.id
                             AND td2.delete_time IS NULL
                             AND td2.task_id = :task_id
                            WHERE d2.deleted_at IS NULL
                            GROUP BY d2.version_group_key
                                 , COALESCE(d2.root_version_id, d2.id)
                        ) vc ON vc.version_group_key = COALESCE(NULLIF(d.version_group_key, ''), CONCAT('root:', COALESCE(d.root_version_id, d.id)::text))
                        LEFT JOIN LATERAL (
                            SELECT
                                COUNT(*)::int AS total_evaluation_points,
                                COUNT(*) FILTER (WHERE rr.passed IS TRUE)::int AS pass_count,
                                COUNT(*) FILTER (WHERE rr.passed IS FALSE AND rr.risk = 'high')::int AS error_count,
                                COUNT(*) FILTER (WHERE rr.passed IS FALSE AND rr.risk IN ('low', 'medium'))::int AS warning_count,
                                0::int AS manual_count,
                                COUNT(*) FILTER (WHERE rr.passed IS FALSE)::int AS issue_count,
                                ARRAY_AGG(rr.fail_message ORDER BY rr.id) FILTER (
                                    WHERE rr.passed IS FALSE AND rr.risk = 'high' AND rr.fail_message IS NOT NULL AND rr.fail_message != ''
                                ) AS error_messages,
                                ARRAY_AGG(rr.fail_message ORDER BY rr.id) FILTER (
                                    WHERE rr.passed IS FALSE AND rr.risk IN ('low', 'medium') AND rr.fail_message IS NOT NULL AND rr.fail_message != ''
                                ) AS warning_messages,
                                ARRAY_AGG(rr.fail_message ORDER BY rr.id) FILTER (
                                    WHERE rr.passed IS FALSE AND rr.fail_message IS NOT NULL AND rr.fail_message != ''
                                ) AS issue_messages,
                                ARRAY[]::text[] AS manual_messages,
                                COALESCE(SUM(rr.score) FILTER (WHERE rr.passed IS TRUE), 0) AS final_score,
                                COALESCE(SUM(rr.score), 0) AS full_score,
                                CASE
                                    WHEN COALESCE(SUM(rr.score), 0) > 0
                                    THEN CONCAT(
                                        ROUND(
                                            COALESCE(SUM(rr.score) FILTER (WHERE rr.passed IS TRUE), 0)::numeric,
                                            1
                                        )::text,
                                        '/',
                                        ROUND(COALESCE(SUM(rr.score), 0)::numeric, 1)::text
                                    )
                                    ELSE '0/0'
                                END AS score_summary,
                                CASE
                                    WHEN COALESCE(SUM(rr.score), 0) > 0
                                    THEN ROUND(
                                        COALESCE(SUM(rr.score) FILTER (WHERE rr.passed IS TRUE), 0)
                                        / SUM(rr.score) * 100,
                                        1
                                    )
                                    ELSE 0
                                END AS score_percent
                            FROM leaudit_rule_results rr
                            WHERE rr.document_id = d.id
                              AND rr.run_id = d.current_run_id
                        ) es ON TRUE
                        LEFT JOIN LATERAL (
                            SELECT COALESCE(SUM(proposed_score_delta), 0) AS approved_delta
                            FROM leaudit_cross_review_proposals p
                            WHERE p.document_id = d.id
                              AND p.status = 'approved'
                              AND p.delete_time IS NULL
                        ) pd ON TRUE
                        WHERE {latestWhereSql}
                        ORDER BY d.created_at DESC, d.id DESC
                        LIMIT :limit OFFSET :offset
                        """
                    ),
                    params,
                )
            ).mappings().all()

            latestDocumentIds = [int(row["document_id"]) for row in rows]
            historyByGroup: dict[str, list[CrossReviewTaskHistoryVersionVO]] = {}
            if latestDocumentIds:
                historyRows = (
                    await session.execute(
                        text(
                            """
                            WITH target_groups AS (
                                SELECT DISTINCT COALESCE(NULLIF(d.version_group_key, ''), CONCAT('root:', COALESCE(d.root_version_id, d.id)::text)) AS version_group_key
                                FROM leaudit_cross_review_task_documents td
                                JOIN leaudit_documents d
                                  ON d.id = td.document_id
                                WHERE td.task_id = :task_id
                                  AND td.delete_time IS NULL
                                  AND d.id = ANY(:document_ids)
                            )
                            SELECT
                                d.id AS document_id,
                                COALESCE(d.normalized_name, '') AS name,
                                CAST(d.biz_document_id AS TEXT) AS document_number,
                                d.type_id,
                                COALESCE(
                                    CASE
                                        WHEN LOWER(COALESCE(ar.status, '')) IN ('pending', 'queued', 'running', 'retrying')
                                            THEN ar.status
                                        ELSE d.processing_status
                                    END,
                                    d.processing_status,
                                    'waiting'
                                ) AS processing_status,
                                d.version_no,
                                d.is_latest_version,
                                COALESCE(NULLIF(d.version_group_key, ''), CONCAT('root:', COALESCE(d.root_version_id, d.id)::text)) AS version_group_key,
                                td.audit_status,
                                d.created_at,
                                COALESCE(dt.name, '') AS type_name,
                                COALESCE(df.file_size, 0) AS file_size,
                                COALESCE(df.file_path, '') AS path,
                                df.file_upload_time AS upload_time,
                                COALESCE(df.file_ext, '') AS file_ext,
                                COALESCE(es.total_evaluation_points, 0) AS total_evaluation_points,
                                COALESCE(es.pass_count, 0) AS pass_count,
                                COALESCE(es.warning_count, 0) AS warning_count,
                                COALESCE(es.error_count, 0) AS error_count,
                                COALESCE(es.manual_count, 0) AS manual_count,
                                COALESCE(es.issue_count, 0) AS issue_count,
                                COALESCE(es.warning_messages, ARRAY[]::text[]) AS warning_messages,
                                COALESCE(es.error_messages, ARRAY[]::text[]) AS error_messages,
                                COALESCE(es.issue_messages, ARRAY[]::text[]) AS issue_messages,
                                COALESCE(es.manual_messages, ARRAY[]::text[]) AS manual_messages,
                                COALESCE(es.final_score, 0) AS final_score,
                                COALESCE(es.full_score, 0) AS full_score,
                                COALESCE(pd.approved_delta, 0) AS approved_delta
                            FROM leaudit_cross_review_task_documents td
                            JOIN leaudit_documents d
                              ON d.id = td.document_id
                            JOIN target_groups tg
                              ON tg.version_group_key = COALESCE(NULLIF(d.version_group_key, ''), CONCAT('root:', COALESCE(d.root_version_id, d.id)::text))
                            LEFT JOIN leaudit_audit_runs ar
                              ON ar.id = d.current_run_id
                            LEFT JOIN leaudit_document_types dt
                              ON dt.id = d.type_id
                            LEFT JOIN LATERAL (
                                SELECT file_size, local_path AS file_path, created_at AS file_upload_time,
                                       COALESCE(file_ext, '') AS file_ext
                                FROM leaudit_document_files
                                WHERE document_id = d.id
                                ORDER BY id ASC
                                LIMIT 1
                            ) df ON TRUE
                            LEFT JOIN LATERAL (
                                SELECT
                                    COUNT(*)::int AS total_evaluation_points,
                                    COUNT(*) FILTER (WHERE rr.passed IS TRUE)::int AS pass_count,
                                    COUNT(*) FILTER (WHERE rr.passed IS FALSE AND rr.risk = 'high')::int AS error_count,
                                    COUNT(*) FILTER (WHERE rr.passed IS FALSE AND rr.risk IN ('low', 'medium'))::int AS warning_count,
                                    0::int AS manual_count,
                                    COUNT(*) FILTER (WHERE rr.passed IS FALSE)::int AS issue_count,
                                    ARRAY_AGG(rr.fail_message ORDER BY rr.id) FILTER (
                                        WHERE rr.passed IS FALSE AND rr.risk = 'high' AND rr.fail_message IS NOT NULL AND rr.fail_message != ''
                                    ) AS error_messages,
                                    ARRAY_AGG(rr.fail_message ORDER BY rr.id) FILTER (
                                        WHERE rr.passed IS FALSE AND rr.risk IN ('low', 'medium') AND rr.fail_message IS NOT NULL AND rr.fail_message != ''
                                    ) AS warning_messages,
                                    ARRAY_AGG(rr.fail_message ORDER BY rr.id) FILTER (
                                        WHERE rr.passed IS FALSE AND rr.fail_message IS NOT NULL AND rr.fail_message != ''
                                    ) AS issue_messages,
                                    ARRAY[]::text[] AS manual_messages,
                                    COALESCE(SUM(rr.score) FILTER (WHERE rr.passed IS TRUE), 0) AS final_score,
                                    COALESCE(SUM(rr.score), 0) AS full_score
                                FROM leaudit_rule_results rr
                                WHERE rr.document_id = d.id
                                  AND rr.run_id = d.current_run_id
                            ) es ON TRUE
                            LEFT JOIN LATERAL (
                                SELECT COALESCE(SUM(proposed_score_delta), 0) AS approved_delta
                                FROM leaudit_cross_review_proposals p
                                WHERE p.document_id = d.id
                                  AND p.status = 'approved'
                                  AND p.delete_time IS NULL
                            ) pd ON TRUE
                            WHERE td.task_id = :task_id
                              AND td.delete_time IS NULL
                              AND d.deleted_at IS NULL
                            ORDER BY COALESCE(NULLIF(d.version_group_key, ''), CONCAT('root:', COALESCE(d.root_version_id, d.id)::text)), d.version_no DESC, d.id DESC
                            """
                        ).bindparams(bindparam("document_ids", expanding=False)),
                        {"task_id": TaskId, "document_ids": latestDocumentIds},
                    )
                ).mappings().all()

                groupedRows: dict[str, list[dict]] = {}
                for row in historyRows:
                    groupedRows.setdefault(str(row.get("version_group_key") or ""), []).append(dict(row))

                for groupKey, groupRows in groupedRows.items():
                    orderedRows = sorted(
                        groupRows,
                        key=lambda item: (int(item.get("version_no") or 1), int(item.get("document_id") or 0)),
                    )
                    localVersionMap = {
                        int(item["document_id"]): index + 1 for index, item in enumerate(orderedRows)
                    }
                    historyItems: list[CrossReviewTaskHistoryVersionVO] = []
                    for item in reversed(orderedRows[:-1]):
                        finalScore = float(item.get("final_score") or 0) + float(item.get("approved_delta") or 0)
                        fullScore = float(item.get("full_score") or 0)
                        historyItems.append(
                            CrossReviewTaskHistoryVersionVO(
                                documentId=int(item["document_id"]),
                                name=str(item.get("name") or ""),
                                documentNumber=item.get("document_number"),
                                typeId=self._to_int(item.get("type_id")),
                                typeName=item.get("type_name"),
                                processingStatus=item.get("processing_status"),
                                versionNo=int(localVersionMap.get(int(item["document_id"]), 1)),
                                auditStatus=int(item.get("audit_status") or 0),
                                createdAt=item.get("created_at"),
                                fileSize=int(item.get("file_size") or 0),
                                path=str(item.get("path") or ""),
                                uploadTime=item.get("upload_time"),
                                fileExt=str(item.get("file_ext") or "") or None,
                                totalEvaluationPoints=int(item.get("total_evaluation_points") or 0),
                                passCount=int(item.get("pass_count") or 0),
                                warningCount=int(item.get("warning_count") or 0),
                                errorCount=int(item.get("error_count") or 0),
                                manualCount=int(item.get("manual_count") or 0),
                                issueCount=int(item.get("issue_count") or 0),
                                warningMessages=self._parse_text_array(item.get("warning_messages")),
                                errorMessages=self._parse_text_array(item.get("error_messages")),
                                issueMessages=self._parse_text_array(item.get("issue_messages")),
                                manualMessages=self._parse_text_array(item.get("manual_messages")),
                                finalScore=finalScore,
                                fullScore=fullScore,
                                scoreSummary=self._build_score_summary(finalScore, fullScore),
                                scorePercent=self._build_score_percent(finalScore, fullScore),
                            )
                        )
                    historyByGroup[groupKey] = historyItems

            items: list[CrossReviewTaskDocumentVO] = []
            for row in rows:
                groupKey = str(row.get("version_group_key") or "")
                historyVersions = historyByGroup.get(groupKey, [])
                finalScore = float(row.get("final_score") or 0) + float(row.get("approved_delta") or 0)
                fullScore = float(row.get("full_score") or 0)
                versionNo = int(row.get("total_versions") or 1)
                items.append(
                    CrossReviewTaskDocumentVO(
                        documentId=int(row["document_id"]),
                        name=str(row["name"] or ""),
                        documentNumber=row.get("document_number"),
                        typeId=self._to_int(row.get("type_id")),
                        typeName=row.get("type_name"),
                        processingStatus=row.get("processing_status"),
                        versionNo=versionNo,
                        isLatestVersion=True,
                        versionGroupKey=groupKey,
                        totalVersions=int(row.get("total_versions") or 1),
                        auditStatus=int(row.get("audit_status") or 0),
                        createdAt=row.get("created_at"),
                        fileSize=int(row.get("file_size") or 0),
                        path=str(row.get("path") or ""),
                        uploadTime=row.get("upload_time"),
                        fileExt=str(row.get("file_ext") or "") or None,
                        totalEvaluationPoints=int(row.get("total_evaluation_points") or 0),
                        passCount=int(row.get("pass_count") or 0),
                        warningCount=int(row.get("warning_count") or 0),
                        errorCount=int(row.get("error_count") or 0),
                        manualCount=int(row.get("manual_count") or 0),
                        issueCount=int(row.get("issue_count") or 0),
                        warningMessages=self._parse_text_array(row.get("warning_messages")),
                        errorMessages=self._parse_text_array(row.get("error_messages")),
                        issueMessages=self._parse_text_array(row.get("issue_messages")),
                        manualMessages=self._parse_text_array(row.get("manual_messages")),
                        finalScore=finalScore,
                        fullScore=fullScore,
                        scoreSummary=self._build_score_summary(finalScore, fullScore),
                        scorePercent=self._build_score_percent(finalScore, fullScore),
                        historyVersions=historyVersions,
                    )
                )
            return CrossReviewTaskDocumentPageVO(
                taskId=TaskId,
                total=total,
                page=Body.page,
                pageSize=Body.pageSize,
                items=items,
            )

    async def CanConfirmTaskDocument(self, CurrentUserId: int, TaskId: int) -> CrossReviewPermissionVO:
        """判断当前用户是否有权确认任务文档完成。"""
        async with GetAsyncSession() as session:
            await self._ensure_tables_ready(session)
            row = (
                await session.execute(
                    text(
                        """
                        SELECT
                            t.assigner_id,
                            COALESCE(
                                MAX(CASE WHEN tm.user_id = :current_user_id AND tm.member_role = 'principal' THEN 1 ELSE 0 END),
                                0
                            ) AS is_principal
                        FROM leaudit_cross_review_tasks t
                        LEFT JOIN leaudit_cross_review_task_members tm
                          ON tm.task_id = t.id
                         AND tm.delete_time IS NULL
                        WHERE t.id = :task_id
                          AND t.delete_time IS NULL
                        GROUP BY t.assigner_id
                        """
                    ),
                    {"task_id": TaskId, "current_user_id": CurrentUserId},
                )
            ).mappings().first()

            if not row:
                raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "交叉评查任务不存在")

            if int(row["assigner_id"]) == CurrentUserId:
                return CrossReviewPermissionVO(canConfirm=True, reason="您是任务创建者，可确认完成")
            if int(row["is_principal"] or 0) == 1:
                return CrossReviewPermissionVO(canConfirm=True, reason="您是任务负责人，可确认完成")
            return CrossReviewPermissionVO(canConfirm=False, reason="您不是任务创建者或负责人")

    async def CompleteTaskDocument(self, CurrentUserId: int, TaskId: int, DocumentId: int) -> CrossReviewTaskCompleteVO:
        """确认任务文档完成。"""
        async with GetAsyncSession() as session:
            await self._ensure_tables_ready(session)
            permission = await self.CanConfirmTaskDocument(CurrentUserId, TaskId)
            if not permission.canConfirm:
                raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, permission.reason)

            await self._reset_transaction_for_write(session)
            async with session.begin():
                mapping = (
                    await session.execute(
                        text(
                            """
                            SELECT id, audit_status
                            FROM leaudit_cross_review_task_documents
                            WHERE task_id = :task_id
                              AND document_id = :document_id
                              AND delete_time IS NULL
                            LIMIT 1
                            """
                        ),
                        {"task_id": TaskId, "document_id": DocumentId},
                    )
                ).mappings().first()
                if not mapping:
                    raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "任务文档不存在")

                await session.execute(
                    text(
                        """
                        UPDATE leaudit_cross_review_task_documents
                        SET audit_status = 1,
                            update_time = NOW()
                        WHERE id = :id
                        """
                    ),
                    {"id": int(mapping["id"])},
                )

                stats = (
                    await session.execute(
                        text(
                            """
                            SELECT
                                COUNT(*)::int AS total_documents,
                                COUNT(*) FILTER (WHERE audit_status = 1)::int AS completed_documents
                            FROM leaudit_cross_review_task_documents
                            WHERE task_id = :task_id
                              AND delete_time IS NULL
                            """
                        ),
                        {"task_id": TaskId},
                    )
                ).mappings().first()

                totalDocuments = int((stats or {}).get("total_documents") or 0)
                completedDocuments = int((stats or {}).get("completed_documents") or 0)
                taskCompleted = totalDocuments > 0 and totalDocuments == completedDocuments
                taskStatus = "completed" if taskCompleted else "in_progress"

                await session.execute(
                    text(
                        """
                        UPDATE leaudit_cross_review_tasks
                        SET status = :status,
                            update_time = NOW()
                        WHERE id = :task_id
                          AND delete_time IS NULL
                        """
                    ),
                    {"task_id": TaskId, "status": taskStatus},
                )

            return CrossReviewTaskCompleteVO(
                taskId=TaskId,
                documentId=DocumentId,
                auditStatus=1,
                taskStatus=taskStatus,
                taskCompleted=taskCompleted,
            )

    async def CreateProposal(self, CurrentUserId: int, Body: CrossReviewProposalCreateDTO) -> CrossReviewProposalCreateVO:
        """创建交叉评查提案。"""
        async with GetAsyncSession() as session:
            await self._ensure_tables_ready(session)
            proposalType = (Body.proposalType or "review_point").strip().lower()
            if proposalType not in {"review_point", "supplement"}:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "proposalType 仅支持 review_point、supplement")
            if Body.deductionScore == 0:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "不允许创建 0 分提案")

            taskId = await self._resolve_task_id_for_document(session, Body.documentId, CurrentUserId)
            await self._ensure_task_member(session, taskId, CurrentUserId)
            await self._ensure_task_document(session, taskId, Body.documentId)

            ruleResult = None
            reviewPointResultId: int | None = None
            evaluationPointName = (Body.evaluationPointName or "").strip()
            extractionResultText = (Body.extractionResultText or "").strip()

            if proposalType == "review_point":
                if Body.reviewPointResultId is None:
                    raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "reviewPointResultId 不能为空")
                reviewPointResultId = int(Body.reviewPointResultId)
                ruleResult = await self._load_rule_result(session, reviewPointResultId, Body.documentId)
                if not evaluationPointName:
                    evaluationPointName = str(ruleResult.get("rule_name") or ruleResult.get("rule_id") or "")
            else:
                if not evaluationPointName:
                    raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "补充意见必须填写评查点名称")
                if not extractionResultText:
                    raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "补充意见必须填写抽取结果")

            duplicateExists = bool(
                await session.scalar(
                    text(
                        """
                        SELECT 1
                        FROM leaudit_cross_review_proposals
                        WHERE task_id = :task_id
                          AND document_id = :document_id
                          AND proposal_type = :proposal_type
                          AND proposer_id = :proposer_id
                          AND status IN ('pending', 'approved')
                          AND delete_time IS NULL
                          AND (
                            (:proposal_type = 'review_point' AND rule_result_id = :rule_result_id)
                            OR (
                              :proposal_type = 'supplement'
                              AND COALESCE(evaluation_point_name, '') = :evaluation_point_name
                              AND COALESCE(extraction_result_text, '') = :extraction_result_text
                            )
                          )
                        LIMIT 1
                        """
                    ),
                    {
                        "task_id": taskId,
                        "document_id": Body.documentId,
                        "proposal_type": proposalType,
                        "rule_result_id": reviewPointResultId,
                        "evaluation_point_name": evaluationPointName,
                        "extraction_result_text": extractionResultText,
                        "proposer_id": CurrentUserId,
                    },
                )
            )
            if duplicateExists:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前内容已存在您的有效提案")

            if proposalType == "review_point":
                currentScore, fullScore = await self._calculate_current_score(session, reviewPointResultId, Body.documentId)
                if Body.deductionScore < 0 and currentScore <= 0:
                    raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前分值已为 0，不能继续扣分")
                if Body.deductionScore > 0 and currentScore >= fullScore:
                    raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前分值已满分，不能继续加分")

            await self._reset_transaction_for_write(session)
            async with session.begin():
                proposalRow = (
                    await session.execute(
                        text(
                            """
                            INSERT INTO leaudit_cross_review_proposals
                                (
                                    task_id,
                                    document_id,
                                    proposal_type,
                                    rule_result_id,
                                    proposer_id,
                                    evaluation_point_name,
                                    extraction_result_text,
                                    proposed_score_delta,
                                    reason,
                                    status
                                )
                            VALUES
                                (
                                    :task_id,
                                    :document_id,
                                    :proposal_type,
                                    :rule_result_id,
                                    :proposer_id,
                                    :evaluation_point_name,
                                    :extraction_result_text,
                                    :proposed_score_delta,
                                    :reason,
                                    'pending'
                                )
                            RETURNING id, create_time
                            """
                        ),
                        {
                            "task_id": taskId,
                            "document_id": Body.documentId,
                            "proposal_type": proposalType,
                            "rule_result_id": reviewPointResultId,
                            "proposer_id": CurrentUserId,
                            "evaluation_point_name": evaluationPointName or None,
                            "extraction_result_text": extractionResultText or None,
                            "proposed_score_delta": Body.deductionScore,
                            "reason": Body.auditOpinion.strip(),
                        },
                    )
                ).mappings().first()
                if not proposalRow:
                    raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, "创建交叉评查提案失败")

                proposalId = int(proposalRow["id"])
                await session.execute(
                    text(
                        """
                        INSERT INTO leaudit_cross_review_votes
                            (proposal_id, voter_id, vote_type)
                        VALUES
                            (:proposal_id, :voter_id, 'agree')
                        """
                    ),
                    {"proposal_id": proposalId, "voter_id": CurrentUserId},
                )
                await self._refresh_proposal_status(session, proposalId)

            return CrossReviewProposalCreateVO(proposalId=proposalId, createdAt=proposalRow.get("create_time"))

    async def VoteProposal(
        self,
        CurrentUserId: int,
        ProposalId: int,
        Body: CrossReviewProposalVoteDTO,
    ) -> CrossReviewProposalVoteVO:
        """对交叉评查提案投票。"""
        voteType = Body.voteType.strip().lower()
        if voteType not in {"agree", "disagree", "cancel"}:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "投票类型仅支持 agree/disagree/cancel")

        async with GetAsyncSession() as session:
            await self._ensure_tables_ready(session)
            proposal = await self._load_proposal(session, ProposalId)
            await self._ensure_task_member(session, int(proposal["task_id"]), CurrentUserId)
            if str(proposal["status"]) in {"approved", "rejected", "cancelled"}:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前提案状态不允许继续投票")

            await self._reset_transaction_for_write(session)
            async with session.begin():
                if voteType == "cancel":
                    deleted = await session.execute(
                        text(
                            """
                            UPDATE leaudit_cross_review_votes
                            SET delete_time = NOW(),
                                update_time = NOW()
                            WHERE proposal_id = :proposal_id
                              AND voter_id = :voter_id
                              AND delete_time IS NULL
                            """
                        ),
                        {"proposal_id": ProposalId, "voter_id": CurrentUserId},
                    )
                    if deleted.rowcount == 0:
                        raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前没有可撤销的投票")
                else:
                    await session.execute(
                        text(
                            """
                            UPDATE leaudit_cross_review_votes
                            SET delete_time = NOW(),
                                update_time = NOW()
                            WHERE proposal_id = :proposal_id
                              AND voter_id = :voter_id
                              AND delete_time IS NULL
                            """
                        ),
                        {"proposal_id": ProposalId, "voter_id": CurrentUserId},
                    )
                    await session.execute(
                        text(
                            """
                            INSERT INTO leaudit_cross_review_votes
                                (proposal_id, voter_id, vote_type)
                            VALUES
                                (:proposal_id, :voter_id, :vote_type)
                            """
                        ),
                        {"proposal_id": ProposalId, "voter_id": CurrentUserId, "vote_type": voteType},
                    )

                proposalStatus = await self._refresh_proposal_status(session, ProposalId)

            return CrossReviewProposalVoteVO(
                proposalId=ProposalId,
                voterId=CurrentUserId,
                voteType=voteType,
                proposalStatus=proposalStatus,
            )

    async def CancelProposal(self, CurrentUserId: int, ProposalId: int) -> CrossReviewProposalCancelVO:
        """撤销交叉评查提案。"""
        async with GetAsyncSession() as session:
            await self._ensure_tables_ready(session)
            proposal = await self._load_proposal(session, ProposalId)
            if int(proposal["proposer_id"]) != CurrentUserId:
                raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "仅提案人可撤销提案")
            if str(proposal["status"]) not in {"pending"}:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前提案状态不允许撤销")

            await self._reset_transaction_for_write(session)
            async with session.begin():
                await session.execute(
                    text(
                        """
                        UPDATE leaudit_cross_review_proposals
                        SET status = 'cancelled',
                            update_time = NOW()
                        WHERE id = :proposal_id
                          AND delete_time IS NULL
                        """
                    ),
                    {"proposal_id": ProposalId},
                )
                await session.execute(
                    text(
                        """
                        UPDATE leaudit_cross_review_votes
                        SET delete_time = NOW(),
                            update_time = NOW()
                        WHERE proposal_id = :proposal_id
                          AND delete_time IS NULL
                        """
                    ),
                    {"proposal_id": ProposalId},
                )

            return CrossReviewProposalCancelVO(proposalId=ProposalId, status="cancelled")

    async def GetDocumentProposals(
        self,
        CurrentUserId: int,
        DocumentId: int,
        Page: int,
        PageSize: int,
    ) -> CrossReviewProposalPageVO:
        """获取文档提案列表。"""
        async with GetAsyncSession() as session:
            await self._ensure_tables_ready(session)
            taskId = await self._resolve_task_id_for_document(session, DocumentId, CurrentUserId)
            await self._ensure_task_member(session, taskId, CurrentUserId)
            return await self._build_document_proposals_page(session, CurrentUserId, taskId, DocumentId, Page, PageSize)

    async def GetDocumentPendingVotes(self, CurrentUserId: int, DocumentId: int) -> CrossReviewPendingVotesVO:
        """获取文档待投票信息。"""
        async with GetAsyncSession() as session:
            await self._ensure_tables_ready(session)
            taskId = await self._resolve_task_id_for_document(session, DocumentId, CurrentUserId)
            await self._ensure_task_member(session, taskId, CurrentUserId)

            proposalPage = await self._build_document_proposals_page(session, CurrentUserId, taskId, DocumentId, 1, 200)
            pendingProposals = [
                CrossReviewPendingProposalVO(
                    evaluationPointName=item.evaluationPointName,
                    pendingVotersNum=len(item.pendingVoters),
                )
                for item in proposalPage.items
                if item.status == "pending" and len(item.pendingVoters) > 0
            ]
            return CrossReviewPendingVotesVO(
                hasPendingVotes=len(pendingProposals) > 0,
                pendingProposals=pendingProposals,
            )

    async def ExportDocumentProposals(self, CurrentUserId: int, DocumentId: int) -> tuple[bytes, str]:
        """导出文档交叉评查意见 Excel。"""
        async with GetAsyncSession() as session:
            await self._ensure_tables_ready(session)
            taskId = await self._resolve_task_id_for_document(session, DocumentId, CurrentUserId)
            await self._ensure_task_member(session, taskId, CurrentUserId)

            documentMeta = await self._load_document_export_meta(session, taskId, DocumentId)
            proposalPage = await self._build_document_proposals_page(session, CurrentUserId, taskId, DocumentId, 1, 1000)

            defaultRows: list[dict[str, object]] = []
            supplementRows: list[dict[str, object]] = []
            voteRows: list[dict[str, object]] = []
            taskMembers = await self._load_task_member_names(session, taskId)

            for item in proposalPage.items:
                row = self._build_export_summary_row(
                    taskId=taskId,
                    taskName=documentMeta["task_name"],
                    documentId=DocumentId,
                    documentName=documentMeta["document_name"],
                    item=item,
                )
                if item.proposalType == "supplement":
                    supplementRows.append(row)
                else:
                    defaultRows.append(row)

                votedNames = {vote.voter for vote in item.votes}
                for vote in item.votes:
                    voteRows.append(
                        {
                            "任务名称": documentMeta["task_name"],
                            "文档名称": documentMeta["document_name"],
                            "意见分类": "补充意见" if item.proposalType == "supplement" else "默认规则意见",
                            "评查点名称": item.evaluationPointName,
                            "发起人": item.proposer,
                            "投票人": vote.voter,
                            "投票结果": self._translate_vote_type(vote.voteType),
                            "投票时间": "",
                        }
                    )
                for _, memberName in taskMembers.items():
                    if memberName in votedNames:
                        continue
                    voteRows.append(
                        {
                            "任务名称": documentMeta["task_name"],
                            "文档名称": documentMeta["document_name"],
                            "意见分类": "补充意见" if item.proposalType == "supplement" else "默认规则意见",
                            "评查点名称": item.evaluationPointName,
                            "发起人": item.proposer,
                            "投票人": memberName,
                            "投票结果": "未投",
                            "投票时间": "",
                        }
                    )

            workbook = self._build_simple_xlsx(
                sheets=[
                    ("默认规则意见", list(self._PROPOSAL_EXPORT_HEADERS), defaultRows),
                    ("补充意见", list(self._PROPOSAL_EXPORT_HEADERS), supplementRows),
                    ("投票明细", list(self._VOTE_EXPORT_HEADERS), voteRows),
                ]
            )
            safeName = quote(f"交叉评查意见_{documentMeta['document_name']}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx")
            return workbook, safeName

    async def UploadTaskDocument(
        self,
        CurrentUserId: int,
        TaskId: int,
        FileName: str,
        FileContent: bytes,
        ContentType: str | None,
        TypeId: int | None = None,
        GroupId: int | None = None,
    ) -> CrossReviewTaskDocumentUploadVO:
        """向交叉评查任务补传文档。"""
        async with GetAsyncSession() as session:
            await self._ensure_tables_ready(session)
            permission = await self.CanConfirmTaskDocument(CurrentUserId, TaskId)
            if not permission.canConfirm:
                raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, permission.reason)

            taskMeta = (
                await session.execute(
                    text(
                        """
                        SELECT id, doc_type_id, doc_type_code
                        FROM leaudit_cross_review_tasks
                        WHERE id = :task_id
                          AND delete_time IS NULL
                        LIMIT 1
                        """
                    ),
                    {"task_id": TaskId},
                )
            ).mappings().first()
            if not taskMeta:
                raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "交叉评查任务不存在")

            resolvedTypeId = int(TypeId) if TypeId is not None else self._to_int(taskMeta.get("doc_type_id"))
            resolvedGroupId = int(GroupId) if GroupId is not None else None
            taskScope = await self._load_task_scope_context(session, TaskId)

        uploadResult = await self.DocumentService.Upload(
            FileName=FileName,
            FileContent=FileContent,
            ContentType=ContentType,
            TypeId=resolvedTypeId,
            TypeCode=None if resolvedTypeId is not None else taskMeta.get("doc_type_code"),
            GroupId=resolvedGroupId,
            CreatedBy=CurrentUserId,
            Region=taskScope["tenant_scope_value"],
            TenantCode=taskScope["tenant_code"],
            TenantName=taskScope["tenant_name"],
            AutoRun=True,
            ReviewScope="cross_review",
        )

        async with GetAsyncSession() as session:
            await self._ensure_tables_ready(session)
            await self._reset_transaction_for_write(session)
            async with session.begin():
                exists = bool(
                    await session.scalar(
                        text(
                            """
                            SELECT 1
                            FROM leaudit_cross_review_task_documents
                            WHERE task_id = :task_id
                              AND document_id = :document_id
                              AND delete_time IS NULL
                            LIMIT 1
                            """
                        ),
                        {"task_id": TaskId, "document_id": uploadResult.documentId},
                    )
                )
                if not exists:
                    await session.execute(
                        text(
                            """
                            INSERT INTO leaudit_cross_review_task_documents
                                (task_id, document_id, audit_status)
                            VALUES
                                (:task_id, :document_id, 0)
                            """
                        ),
                        {"task_id": TaskId, "document_id": uploadResult.documentId},
                    )
                await session.execute(
                    text(
                        """
                        UPDATE leaudit_cross_review_tasks
                        SET status = 'in_progress',
                            update_time = NOW()
                        WHERE id = :task_id
                          AND delete_time IS NULL
                        """
                    ),
                    {"task_id": TaskId},
                )

        return CrossReviewTaskDocumentUploadVO(
            taskId=TaskId,
            documentId=uploadResult.documentId,
            auditStatus=0,
            processingStatus=uploadResult.processingStatus,
        )

    async def AppendTaskDocumentAttachments(
        self,
        CurrentUserId: int,
        TaskId: int,
        DocumentId: int,
        Files: list[tuple[str, bytes, str | None]],
        Remark: str | None = None,
    ) -> CrossReviewTaskDocumentAppendVO:
        """为交叉评查任务文档追加附件，并生成同版本链新版本。"""
        async with GetAsyncSession() as session:
            await self._ensure_tables_ready(session)
            permission = await self.CanConfirmTaskDocument(CurrentUserId, TaskId)
            if not permission.canConfirm:
                raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, permission.reason)
            await self._ensure_task_document(session, TaskId, DocumentId)

        appendResult = await self.DocumentService.AppendAttachments(
            CurrentUserId=CurrentUserId,
            Id=DocumentId,
            Files=Files,
            MergeMode="new",
            Remark=Remark,
        )

        async with GetAsyncSession() as session:
            await self._ensure_tables_ready(session)
            await self._reset_transaction_for_write(session)
            async with session.begin():
                exists = bool(
                    await session.scalar(
                        text(
                            """
                            SELECT 1
                            FROM leaudit_cross_review_task_documents
                            WHERE task_id = :task_id
                              AND document_id = :document_id
                              AND delete_time IS NULL
                            LIMIT 1
                            """
                        ),
                        {"task_id": TaskId, "document_id": appendResult.documentId},
                    )
                )
                if not exists:
                    await session.execute(
                        text(
                            """
                            INSERT INTO leaudit_cross_review_task_documents
                                (task_id, document_id, audit_status)
                            VALUES
                                (:task_id, :document_id, 0)
                            """
                        ),
                        {"task_id": TaskId, "document_id": appendResult.documentId},
                    )
                await session.execute(
                    text(
                        """
                        UPDATE leaudit_cross_review_tasks
                        SET status = 'in_progress',
                            update_time = NOW()
                        WHERE id = :task_id
                          AND delete_time IS NULL
                        """
                    ),
                    {"task_id": TaskId},
                )

        return CrossReviewTaskDocumentAppendVO(
            taskId=TaskId,
            originalDocumentId=DocumentId,
            documentId=appendResult.documentId,
            versionNo=appendResult.versionNo,
            versionGroupKey=appendResult.versionGroupKey,
            auditStatus=0,
            processingStatus=appendResult.processingStatus,
        )

    async def _build_document_proposals_page(
        self,
        session,
        CurrentUserId: int,
        TaskId: int,
        DocumentId: int,
        Page: int,
        PageSize: int,
    ) -> CrossReviewProposalPageVO:
        params = {
            "task_id": TaskId,
            "document_id": DocumentId,
            "limit": PageSize,
            "offset": (Page - 1) * PageSize,
        }
        total = int(
            (
                await session.scalar(
                    text(
                        """
                        SELECT COUNT(*)
                        FROM leaudit_cross_review_proposals p
                        WHERE p.task_id = :task_id
                          AND p.document_id = :document_id
                          AND p.delete_time IS NULL
                        """
                    ),
                    params,
                )
            )
            or 0
        )
        proposalRows = (
            await session.execute(
                text(
                    """
                    SELECT
                        p.id,
                        p.task_id,
                        p.document_id,
                        p.proposal_type,
                        p.rule_result_id,
                        p.proposer_id,
                        p.evaluation_point_name,
                        p.extraction_result_text,
                        p.proposed_score_delta,
                        p.reason,
                        p.status,
                        p.create_time,
                        rr.rule_name,
                        rr.fail_message,
                        COALESCE(u.nick_name, u.username, '') AS proposer_name
                    FROM leaudit_cross_review_proposals p
                    LEFT JOIN leaudit_rule_results rr
                      ON rr.id = p.rule_result_id
                    LEFT JOIN sso_users u
                      ON u.id = p.proposer_id
                    WHERE p.task_id = :task_id
                      AND p.document_id = :document_id
                      AND p.delete_time IS NULL
                    ORDER BY p.id DESC
                    LIMIT :limit OFFSET :offset
                    """
                ),
                params,
            )
        ).mappings().all()
        taskMembers = await self._load_task_member_names(session, TaskId)
        proposalIds = [int(row["id"]) for row in proposalRows]
        votesMap = await self._load_votes_map(session, proposalIds)

        items: list[CrossReviewProposalItemVO] = []
        for row in proposalRows:
            proposalId = int(row["id"])
            voteRows = votesMap.get(proposalId, [])
            agreeVoters = [str(vote["voter_name"]) for vote in voteRows if vote["vote_type"] == "agree"]
            disagreeVoters = [str(vote["voter_name"]) for vote in voteRows if vote["vote_type"] == "disagree"]
            votedUserIds = {int(vote["voter_id"]) for vote in voteRows}
            pendingVoters = [name for userId, name in taskMembers.items() if userId not in votedUserIds]
            canVote = CurrentUserId in taskMembers and CurrentUserId not in votedUserIds and str(row["status"]) == "pending"
            items.append(
                CrossReviewProposalItemVO(
                    proposalId=proposalId,
                    proposalType=str(row.get("proposal_type") or "review_point"),
                    reviewPointResultId=self._to_int(row.get("rule_result_id")),
                    evaluationPointName=str(row.get("evaluation_point_name") or row.get("rule_name") or ""),
                    extractionResultText=str(row.get("extraction_result_text") or ""),
                    proposedScore=float(row["proposed_score_delta"] or 0),
                    reason=str(row.get("reason") or ""),
                    proposer=str(row.get("proposer_name") or ""),
                    votes=[
                        CrossReviewProposalVoteItemVO(
                            voter=str(vote["voter_name"]),
                            voteType=str(vote["vote_type"]),
                        )
                        for vote in voteRows
                    ],
                    agreeVoters=agreeVoters,
                    disagreeVoters=disagreeVoters,
                    pendingVoters=pendingVoters,
                    canVote=canVote,
                    problemMessage=str(row.get("fail_message") or row.get("extraction_result_text") or row.get("rule_name") or ""),
                    proposerId=int(row["proposer_id"]),
                    createdAt=row.get("create_time"),
                    status=str(row.get("status") or "pending"),
                )
            )
        return CrossReviewProposalPageVO(total=total, page=Page, pageSize=PageSize, items=items)

    async def _resolve_task_id_for_document(self, session, documentId: int, userId: int) -> int:
        row = (
            await session.execute(
                text(
                    """
                    SELECT td.task_id
                    FROM leaudit_cross_review_task_documents td
                    JOIN leaudit_cross_review_task_members tm
                      ON tm.task_id = td.task_id
                    JOIN leaudit_cross_review_tasks t
                      ON t.id = td.task_id
                    WHERE td.document_id = :document_id
                      AND td.delete_time IS NULL
                      AND tm.user_id = :user_id
                      AND tm.delete_time IS NULL
                      AND t.delete_time IS NULL
                    ORDER BY td.task_id DESC
                    LIMIT 1
                    """
                ),
                {"document_id": documentId, "user_id": userId},
            )
        ).mappings().first()
        if not row:
            raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "当前文档不在您的交叉评查任务中")
        return int(row["task_id"])

    async def _ensure_task_document(self, session, taskId: int, documentId: int) -> None:
        exists = bool(
            await session.scalar(
                text(
                    """
                    SELECT 1
                    FROM leaudit_cross_review_task_documents
                    WHERE task_id = :task_id
                      AND document_id = :document_id
                      AND delete_time IS NULL
                    LIMIT 1
                    """
                ),
                {"task_id": taskId, "document_id": documentId},
            )
        )
        if not exists:
            raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "当前文档不在交叉评查任务内")

    async def _load_rule_result(self, session, ruleResultId: int, documentId: int):
        row = (
            await session.execute(
                text(
                    """
                    SELECT id, document_id, rule_id, rule_name, score, passed
                    FROM leaudit_rule_results
                    WHERE id = :rule_result_id
                      AND document_id = :document_id
                    LIMIT 1
                    """
                ),
                {"rule_result_id": ruleResultId, "document_id": documentId},
            )
        ).mappings().first()
        if not row:
            raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "对应评查点结果不存在")
        return row

    async def _calculate_current_score(self, session, ruleResultId: int, documentId: int) -> tuple[float, float]:
        row = (
            await session.execute(
                text(
                    """
                    WITH approved_delta AS (
                        SELECT COALESCE(SUM(proposed_score_delta), 0) AS delta
                        FROM leaudit_cross_review_proposals
                        WHERE rule_result_id = :rule_result_id
                          AND document_id = :document_id
                          AND status = 'approved'
                          AND delete_time IS NULL
                    )
                    SELECT
                        COALESCE(rr.score, 0) AS full_score,
                        CASE
                            WHEN a.edit_audit_status = 1 THEN CASE WHEN a.override_result IS TRUE THEN COALESCE(rr.score, 0) ELSE 0 END
                            WHEN rr.passed IS TRUE THEN COALESCE(rr.score, 0)
                            ELSE 0
                        END + COALESCE(ad.delta, 0) AS current_score
                    FROM leaudit_rule_results rr
                    LEFT JOIN leaudit_review_point_audits a
                      ON a.rule_result_id = rr.id
                    CROSS JOIN approved_delta ad
                    WHERE rr.id = :rule_result_id
                      AND rr.document_id = :document_id
                    LIMIT 1
                    """
                ),
                {"rule_result_id": ruleResultId, "document_id": documentId},
            )
        ).mappings().first()
        if not row:
            raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "对应评查点结果不存在")
        return float(row.get("current_score") or 0), float(row.get("full_score") or 0)

    async def _load_proposal(self, session, proposalId: int):
        row = (
            await session.execute(
                text(
                    """
                    SELECT id, task_id, document_id, proposer_id, status
                    FROM leaudit_cross_review_proposals
                    WHERE id = :proposal_id
                      AND delete_time IS NULL
                    LIMIT 1
                    """
                ),
                {"proposal_id": proposalId},
            )
        ).mappings().first()
        if not row:
            raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "交叉评查提案不存在")
        return row

    async def _load_document_export_meta(self, session, taskId: int, documentId: int) -> dict[str, str]:
        row = (
            await session.execute(
                text(
                    """
                    SELECT
                        t.task_name,
                        COALESCE(f.file_name, d.normalized_name, CAST(d.id AS TEXT)) AS document_name
                    FROM leaudit_cross_review_tasks t
                    JOIN leaudit_documents d
                      ON d.id = :document_id
                    LEFT JOIN leaudit_document_files f
                      ON f.document_id = d.id
                     AND f.is_active = true
                     AND f.deleted_at IS NULL
                     AND f.file_role IN ('original', 'primary')
                    WHERE t.id = :task_id
                      AND t.delete_time IS NULL
                      AND d.deleted_at IS NULL
                    LIMIT 1
                    """
                ),
                {"task_id": taskId, "document_id": documentId},
            )
        ).mappings().first()
        if not row:
            raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "导出目标不存在")
        return {
            "task_name": str(row.get("task_name") or f"任务{taskId}"),
            "document_name": str(row.get("document_name") or f"文档{documentId}"),
        }

    async def _load_task_member_names(self, session, taskId: int) -> dict[int, str]:
        rows = (
            await session.execute(
                text(
                    """
                    SELECT tm.user_id, COALESCE(u.nick_name, u.username, CAST(tm.user_id AS TEXT)) AS display_name
                    FROM leaudit_cross_review_task_members tm
                    LEFT JOIN sso_users u
                      ON u.id = tm.user_id
                    WHERE tm.task_id = :task_id
                      AND tm.delete_time IS NULL
                    ORDER BY tm.id ASC
                    """
                ),
                {"task_id": taskId},
            )
        ).mappings().all()
        return {int(row["user_id"]): str(row.get("display_name") or row["user_id"]) for row in rows}

    async def _load_votes_map(self, session, proposalIds: list[int]) -> dict[int, list[dict[str, object]]]:
        if not proposalIds:
            return {}
        rows = (
            await session.execute(
                text(
                    """
                    SELECT
                        v.proposal_id,
                        v.voter_id,
                        v.vote_type,
                        COALESCE(u.nick_name, u.username, CAST(v.voter_id AS TEXT)) AS voter_name
                    FROM leaudit_cross_review_votes v
                    LEFT JOIN sso_users u
                      ON u.id = v.voter_id
                    WHERE v.proposal_id IN :proposal_ids
                      AND v.delete_time IS NULL
                    ORDER BY v.id ASC
                    """
                ).bindparams(bindparam("proposal_ids", expanding=True)),
                {"proposal_ids": proposalIds},
            )
        ).mappings().all()
        result: dict[int, list[dict[str, object]]] = {}
        for row in rows:
            result.setdefault(int(row["proposal_id"]), []).append(dict(row))
        return result

    async def _refresh_proposal_status(self, session, proposalId: int) -> str:
        row = (
            await session.execute(
                text(
                    """
                    SELECT p.id, p.task_id, p.status
                    FROM leaudit_cross_review_proposals p
                    WHERE p.id = :proposal_id
                      AND p.delete_time IS NULL
                    LIMIT 1
                    """
                ),
                {"proposal_id": proposalId},
            )
        ).mappings().first()
        if not row:
            raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "交叉评查提案不存在")
        currentStatus = str(row["status"])
        if currentStatus == "cancelled":
            return currentStatus

        taskId = int(row["task_id"])
        memberCount = int(
            (
                await session.scalar(
                    text(
                        """
                        SELECT COUNT(*)
                        FROM leaudit_cross_review_task_members
                        WHERE task_id = :task_id
                          AND delete_time IS NULL
                        """
                    ),
                    {"task_id": taskId},
                )
            )
            or 0
        )
        threshold = floor(memberCount / 2) + 1 if memberCount > 0 else 1
        voteStats = (
            await session.execute(
                text(
                    """
                    SELECT
                        COUNT(*) FILTER (WHERE vote_type = 'agree')::int AS agree_count,
                        COUNT(*) FILTER (WHERE vote_type = 'disagree')::int AS disagree_count
                    FROM leaudit_cross_review_votes
                    WHERE proposal_id = :proposal_id
                      AND delete_time IS NULL
                    """
                ),
                {"proposal_id": proposalId},
            )
        ).mappings().first()
        agreeCount = int((voteStats or {}).get("agree_count") or 0)
        disagreeCount = int((voteStats or {}).get("disagree_count") or 0)
        remaining = max(memberCount - agreeCount - disagreeCount, 0)

        if agreeCount >= threshold:
            nextStatus = "approved"
        elif disagreeCount >= threshold:
            nextStatus = "rejected"
        elif agreeCount + remaining < threshold:
            nextStatus = "rejected"
        else:
            nextStatus = "pending"

        if nextStatus != currentStatus:
            await session.execute(
                text(
                    """
                    UPDATE leaudit_cross_review_proposals
                    SET status = :status,
                        update_time = NOW()
                    WHERE id = :proposal_id
                    """
                ),
                {"proposal_id": proposalId, "status": nextStatus},
            )
        return nextStatus

    async def _ensure_tables_ready(self, session) -> None:
        """确保第一阶段所需表已初始化。"""
        required = [
            "leaudit_cross_review_tasks",
            "leaudit_cross_review_task_members",
            "leaudit_cross_review_task_documents",
            "leaudit_cross_review_proposals",
            "leaudit_cross_review_votes",
        ]
        missing_tables: list[str] = []
        for tableName in required:
            exists = bool(
                await session.scalar(
                    text(
                        """
                        SELECT EXISTS (
                            SELECT 1
                            FROM information_schema.tables
                            WHERE table_schema = current_schema()
                              AND table_name = :table_name
                        )
                        """
                    ),
                    {"table_name": tableName},
                )
            )
            if not exists:
                missing_tables.append(tableName)

        if missing_tables:
            for statement in self._SCHEMA_BOOTSTRAP_STATEMENTS:
                await session.execute(text(statement))
            await session.commit()

        for tableName in required:
            exists = bool(
                await session.scalar(
                    text(
                        """
                        SELECT EXISTS (
                            SELECT 1
                            FROM information_schema.tables
                            WHERE table_schema = current_schema()
                              AND table_name = :table_name
                        )
                        """
                    ),
                    {"table_name": tableName},
                )
            )
            if not exists:
                raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, f"交叉评查表未初始化: {tableName}")

        proposalColumns = await self._load_table_columns(session, "leaudit_cross_review_proposals")
        if "proposal_type" not in proposalColumns:
            await session.execute(
                text("ALTER TABLE leaudit_cross_review_proposals ADD COLUMN proposal_type VARCHAR(32) NOT NULL DEFAULT 'review_point'")
            )
        if "evaluation_point_name" not in proposalColumns:
            await session.execute(
                text("ALTER TABLE leaudit_cross_review_proposals ADD COLUMN evaluation_point_name VARCHAR(255)")
            )
        if "extraction_result_text" not in proposalColumns:
            await session.execute(
                text("ALTER TABLE leaudit_cross_review_proposals ADD COLUMN extraction_result_text TEXT")
            )
        if proposalColumns:
            isRuleResultNotNull = bool(
                await session.scalar(
                    text(
                        """
                        SELECT 1
                        FROM information_schema.columns
                        WHERE table_schema = current_schema()
                          AND table_name = 'leaudit_cross_review_proposals'
                          AND column_name = 'rule_result_id'
                          AND is_nullable = 'NO'
                        LIMIT 1
                        """
                    )
                )
            )
            if isRuleResultNotNull:
                await session.execute(
                    text("ALTER TABLE leaudit_cross_review_proposals ALTER COLUMN rule_result_id DROP NOT NULL")
                )
        await session.commit()

    async def _ensure_task_member(self, session, taskId: int, userId: int) -> None:
        """校验当前用户是否是任务成员。"""
        exists = bool(
            await session.scalar(
                text(
                    """
                    SELECT 1
                    FROM leaudit_cross_review_task_members tm
                    JOIN leaudit_cross_review_tasks t
                      ON t.id = tm.task_id
                    WHERE tm.task_id = :task_id
                      AND tm.user_id = :user_id
                      AND tm.delete_time IS NULL
                      AND t.delete_time IS NULL
                    LIMIT 1
                    """
                ),
                {"task_id": taskId, "user_id": userId},
            )
        )
        if not exists:
            raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前用户不是交叉评查任务成员")

    async def _assert_task_scope_inputs(
        self,
        session,
        current_user_id: int,
        member_user_ids: list[int],
        document_ids: list[int],
    ) -> None:
        """创建任务前校验成员和文档均在单一有效租户范围内。"""
        current_context = await self.DocumentService._getCurrentUserContext(current_user_id)

        current_tenant_code = str(current_context.get("tenant_code") or "").strip()
        current_tenant_name = str(current_context.get("tenant_name") or current_context.get("tenant_scope_value") or current_context.get("area") or "").strip()
        is_global = bool(current_context.get("is_global"))
        if not is_global and not current_tenant_code and not current_tenant_name:
            raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前用户未绑定有效租户，不能创建交叉评查任务")

        resolved_scopes: list[dict[str, str]] = []

        if member_user_ids:
            sso_user_columns = await SsoUserCompat.get_columns(session)
            tenant_code_select = SsoUserCompat.optional_coalesce_as(
                sso_user_columns,
                alias="u",
                column="tenant_code",
                fallback_sql="''",
            )
            tenant_name_select = SsoUserCompat.optional_coalesce_as(
                sso_user_columns,
                alias="u",
                column="tenant_name",
                fallback_sql="''",
            )
            user_rows = (
                await session.execute(
                    text(
                        f"""
                        SELECT u.id, COALESCE(u.area, '') AS area, {tenant_code_select}, {tenant_name_select}
                        FROM sso_users u
                        WHERE id IN :user_ids
                          AND deleted_at IS NULL
                        """
                    ).bindparams(bindparam("user_ids", expanding=True)),
                    {"user_ids": member_user_ids},
                )
            ).mappings().all()
            user_map = {int(row["id"]): row for row in user_rows}
            missing_user_ids = [user_id for user_id in member_user_ids if user_id not in user_map]
            if missing_user_ids:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, f"成员用户不存在: {missing_user_ids[0]}")

            for user_id in member_user_ids:
                row = user_map[user_id]
                tenant = await self.TenantResolver.ResolveUserContext(
                    Area=str(row.get("area") or ""),
                    TenantCode=str(row.get("tenant_code") or "") or None,
                    TenantName=str(row.get("tenant_name") or "") or None,
                    Source="cross_review_member_scope",
                )
                member_tenant_code = str(tenant.tenant_code or row.get("tenant_code") or "").strip()
                member_tenant_name = str(tenant.tenant_name or tenant.normalized_value or row.get("area") or "").strip()
                if not member_tenant_code and not member_tenant_name:
                    raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, f"成员用户未绑定有效租户: {user_id}")
                resolved_scopes.append({"tenant_code": member_tenant_code, "tenant_name": member_tenant_name})
                if current_tenant_code:
                    if member_tenant_code:
                        if member_tenant_code != current_tenant_code:
                            raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, f"不能将其他租户用户加入交叉评查任务: {user_id}")
                    elif member_tenant_name != current_tenant_name:
                        raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, f"不能将其他租户用户加入交叉评查任务: {user_id}")
                elif not is_global and member_tenant_name != current_tenant_name:
                    raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, f"不能将其他租户用户加入交叉评查任务: {user_id}")

        if document_ids:
            document_columns = await self._load_table_columns(session, "leaudit_documents")
            document_tenant_code_select = "COALESCE(tenant_code, '') AS tenant_code" if "tenant_code" in document_columns else "'' AS tenant_code"
            document_rows = (
                await session.execute(
                    text(
                        f"""
                        SELECT id, COALESCE(region, '') AS region, {document_tenant_code_select}
                        FROM leaudit_documents
                        WHERE id IN :document_ids
                          AND deleted_at IS NULL
                        """
                    ).bindparams(bindparam("document_ids", expanding=True)),
                    {"document_ids": document_ids},
                )
            ).mappings().all()
            document_map = {int(row["id"]): row for row in document_rows}
            missing_document_ids = [document_id for document_id in document_ids if document_id not in document_map]
            if missing_document_ids:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, f"任务文档不存在: {missing_document_ids[0]}")

            for document_id in document_ids:
                row = document_map[document_id]
                tenant = await self.TenantResolver.Resolve(
                    RawValue=str(row.get("region") or ""),
                    Source="cross_review_document_scope",
                    PreferredTenantCode=str(row.get("tenant_code") or "") or None,
                )
                document_tenant_code = str(tenant.tenant_code or row.get("tenant_code") or "").strip()
                document_tenant_name = str(tenant.tenant_name or tenant.normalized_value or row.get("region") or "").strip()
                if not document_tenant_code and not document_tenant_name:
                    raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, f"任务文档未绑定有效租户: {document_id}")
                resolved_scopes.append({"tenant_code": document_tenant_code, "tenant_name": document_tenant_name})
                if current_tenant_code:
                    if document_tenant_code:
                        if document_tenant_code != current_tenant_code:
                            raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, f"不能将其他租户文档加入交叉评查任务: {document_id}")
                    elif document_tenant_name != current_tenant_name:
                        raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, f"不能将其他租户文档加入交叉评查任务: {document_id}")
                elif not is_global and document_tenant_name != current_tenant_name:
                    raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, f"不能将其他租户文档加入交叉评查任务: {document_id}")

        self._assert_single_task_scope(resolved_scopes)

    async def _load_task_scope_context(self, session, task_id: int) -> dict[str, str | None]:
        """读取任务所属租户上下文，补传文档时沿用任务真实边界。"""
        document_columns = await self._load_table_columns(session, "leaudit_documents")
        document_tenant_code_select = "COALESCE(d.tenant_code, '') AS tenant_code" if "tenant_code" in document_columns else "'' AS tenant_code"
        document_rows = (
            await session.execute(
                text(
                    f"""
                    SELECT
                        td.document_id,
                        COALESCE(d.region, '') AS region,
                        {document_tenant_code_select}
                    FROM leaudit_cross_review_task_documents td
                    JOIN leaudit_documents d
                      ON d.id = td.document_id
                    WHERE td.task_id = :task_id
                      AND td.delete_time IS NULL
                      AND d.deleted_at IS NULL
                    ORDER BY td.id ASC
                    """
                ),
                {"task_id": task_id},
            )
        ).mappings().all()
        if document_rows:
            document_scopes: list[dict[str, str]] = []
            for row in document_rows:
                tenant = await self.TenantResolver.Resolve(
                    RawValue=str(row.get("region") or ""),
                    Source="cross_review_task_document_scope",
                    PreferredTenantCode=str(row.get("tenant_code") or "") or None,
                )
                document_scopes.append(
                    {
                        "tenant_code": str(tenant.tenant_code or row.get("tenant_code") or "").strip(),
                        "tenant_name": str(tenant.tenant_name or tenant.normalized_value or row.get("region") or "").strip(),
                    }
                )
            return self._assert_single_task_scope(document_scopes, error_message="交叉评查任务存在多租户文档，不能继续补传文档")

        sso_user_columns = await SsoUserCompat.get_columns(session)
        tenant_code_select = SsoUserCompat.optional_coalesce_as(
            sso_user_columns,
            alias="u",
            column="tenant_code",
            fallback_sql="''",
        )
        tenant_name_select = SsoUserCompat.optional_coalesce_as(
            sso_user_columns,
            alias="u",
            column="tenant_name",
            fallback_sql="''",
        )
        row = (
            await session.execute(
                text(
                    f"""
                    SELECT
                        COALESCE(u.area, '') AS area,
                        {tenant_code_select},
                        {tenant_name_select}
                    FROM leaudit_cross_review_tasks t
                    JOIN sso_users u
                      ON u.id = t.assigner_id
                    WHERE t.id = :task_id
                      AND t.delete_time IS NULL
                    LIMIT 1
                    """
                ),
                {"task_id": task_id},
            )
        ).mappings().first()
        if not row:
            raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "交叉评查任务不存在")

        tenant = await self.TenantResolver.ResolveUserContext(
            Area=str(row.get("area") or ""),
            TenantCode=str(row.get("tenant_code") or "") or None,
            TenantName=str(row.get("tenant_name") or "") or None,
            Source="cross_review_task_scope",
        )
        return {
            "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 str(row.get("area") or "") or None),
            "tenant_scope_value": tenant.tenant_name or tenant.normalized_value or str(row.get("area") or ""),
        }

    def _assert_single_task_scope(
        self,
        scopes: list[dict[str, str]],
        *,
        error_message: str = "交叉评查任务不允许混挂多个租户的成员或文档",
    ) -> dict[str, str | None]:
        normalized: list[dict[str, str]] = []
        identities: set[str] = set()
        for scope in scopes:
            tenant_code = str(scope.get("tenant_code") or "").strip()
            tenant_name = str(scope.get("tenant_name") or "").strip()
            if not tenant_code and not tenant_name:
                continue
            identity = f"code:{tenant_code}" if tenant_code else f"name:{tenant_name}"
            identities.add(identity)
            normalized.append({"tenant_code": tenant_code, "tenant_name": tenant_name})
        if len(identities) > 1:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, error_message)
        if not normalized:
            raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "未能识别交叉评查任务租户边界")
        primary = normalized[0]
        return {
            "tenant_code": primary["tenant_code"] or None,
            "tenant_name": primary["tenant_name"] or None,
            "tenant_scope_value": primary["tenant_name"] or primary["tenant_code"] or "",
        }

    async def _reset_transaction_for_write(self, session) -> None:
        """显式写事务前清理查询阶段开启的隐式事务。"""
        if session.in_transaction():
            await session.rollback()

    def _unique_int_list(self, values: list[int]) -> list[int]:
        """去重并保留原顺序。"""
        seen: set[int] = set()
        result: list[int] = []
        for value in values:
            intValue = int(value)
            if intValue in seen:
                continue
            seen.add(intValue)
            result.append(intValue)
        return result

    def _to_int(self, value) -> int | None:
        """安全转 int。"""
        if value is None:
            return None
        return int(value)

    def _parse_text_array(self, value) -> list[str]:
        """安全解析 PostgreSQL text[] 为字符串列表。"""
        if value is None:
            return []
        if isinstance(value, list):
            return [str(v) for v in value]
        return [str(value)]

    def _parse_task_tenants(self, value) -> list[CrossReviewTaskTenantVO]:
        """安全解析任务租户 JSON 聚合结果。"""
        if value is None:
            return []
        if not isinstance(value, list):
            return []
        result: list[CrossReviewTaskTenantVO] = []
        seen: set[tuple[str, str]] = set()
        for item in value:
            if not isinstance(item, dict):
                continue
            tenant_code = str(item.get("tenantCode") or item.get("tenant_code") or "").strip()
            tenant_name = str(item.get("tenantName") or item.get("tenant_name") or "").strip()
            if not tenant_code and not tenant_name:
                continue
            identity = (tenant_code, tenant_name)
            if identity in seen:
                continue
            seen.add(identity)
            result.append(CrossReviewTaskTenantVO(tenantCode=tenant_code, tenantName=tenant_name or tenant_code))
        return result

    def _build_score_summary(self, finalScore: float, fullScore: float) -> str:
        if fullScore <= 0:
            return "0/0"
        return f"{round(finalScore, 1)}/{round(fullScore, 1)}"

    def _build_score_percent(self, finalScore: float, fullScore: float) -> float:
        if fullScore <= 0:
            return 0.0
        return round(finalScore / fullScore * 100, 1)

    def _build_export_summary_row(
        self,
        taskId: int,
        taskName: str,
        documentId: int,
        documentName: str,
        item: CrossReviewProposalItemVO,
    ) -> dict[str, object]:
        return {
            "任务名称": taskName,
            "文档名称": documentName,
            "意见分类": "补充意见" if item.proposalType == "supplement" else "默认规则意见",
            "评查点名称": item.evaluationPointName,
            "问题依据/命中内容": item.extractionResultText or item.problemMessage or "",
            "原问题说明": item.problemMessage or "",
            "评查意见": item.reason,
            "分数调整": item.proposedScore,
            "提案状态": self._translate_proposal_status(item.status),
            "发起人": item.proposer,
            "提出时间": self._format_datetime(item.createdAt),
            "同意人数": len(item.agreeVoters),
            "同意人员": "、".join(item.agreeVoters),
            "不同意人数": len(item.disagreeVoters),
            "不同意人员": "、".join(item.disagreeVoters),
            "未投人数": len(item.pendingVoters),
            "未投人员": "、".join(item.pendingVoters),
        }

    def _translate_vote_type(self, voteType: str) -> str:
        return {
            "agree": "同意",
            "disagree": "不同意",
            "cancel": "撤销",
        }.get(voteType, voteType or "")

    def _translate_proposal_status(self, status: str) -> str:
        return {
            "pending": "待定",
            "approved": "已通过",
            "rejected": "已驳回",
            "cancelled": "已撤销",
        }.get(status, status or "")

    def _format_datetime(self, value) -> str:
        if value is None:
            return ""
        if isinstance(value, datetime):
            return value.strftime("%Y-%m-%d %H:%M:%S")
        return str(value)

    def _build_simple_xlsx(self, sheets: list[tuple[str, list[str], list[dict[str, object]]]]) -> bytes:
        normalized_sheets: list[tuple[str, list[str], list[list[str]]]] = []
        for sheetName, headers, rows in sheets:
            tableRows = [[self._excel_cell_text(row.get(header)) for header in headers] for row in rows]
            normalized_sheets.append((sheetName, headers, tableRows))

        sharedStrings: list[str] = []
        stringIndex: dict[str, int] = {}

        def intern(value: str) -> int:
            if value in stringIndex:
                return stringIndex[value]
            idx = len(sharedStrings)
            stringIndex[value] = idx
            sharedStrings.append(value)
            return idx

        for _, headers, rows in normalized_sheets:
            for header in headers:
                intern(header)
            for row in rows:
                for cell in row:
                    intern(cell)

        buffer = io.BytesIO()
        with ZipFile(buffer, "w", ZIP_DEFLATED) as zf:
            zf.writestr("[Content_Types].xml", self._xlsx_content_types(len(normalized_sheets)))
            zf.writestr("_rels/.rels", self._xlsx_root_rels())
            zf.writestr("xl/workbook.xml", self._xlsx_workbook([sheet[0] for sheet in normalized_sheets]))
            zf.writestr("xl/_rels/workbook.xml.rels", self._xlsx_workbook_rels(len(normalized_sheets)))
            zf.writestr("xl/styles.xml", self._xlsx_styles())
            zf.writestr("xl/sharedStrings.xml", self._xlsx_shared_strings(sharedStrings))
            for index, (_, headers, rows) in enumerate(normalized_sheets, start=1):
                zf.writestr(f"xl/worksheets/sheet{index}.xml", self._xlsx_sheet(headers, rows, intern))
            zf.writestr("docProps/core.xml", self._xlsx_core_props())
            zf.writestr("docProps/app.xml", self._xlsx_app_props([sheet[0] for sheet in normalized_sheets]))
        return buffer.getvalue()

    def _excel_cell_text(self, value: object) -> str:
        if value is None:
            return ""
        if isinstance(value, float):
            return str(round(value, 2))
        return str(value)

    def _xlsx_content_types(self, sheetCount: int) -> str:
        overrides = [
            '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>',
            '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>',
            '<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>',
            '<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>',
            '<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>',
        ]
        overrides.extend(
            f'<Override PartName="/xl/worksheets/sheet{index}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>'
            for index in range(1, sheetCount + 1)
        )
        return (
            '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
            '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
            '<Default Extension="xml" ContentType="application/xml"/>'
            + "".join(overrides)
            + "</Types>"
        )

    def _xlsx_root_rels(self) -> str:
        return (
            '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
            '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'
            '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>'
            '<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>'
            "</Relationships>"
        )

    def _xlsx_workbook(self, sheetNames: list[str]) -> str:
        sheets = "".join(
            f'<sheet name="{escape(name)}" sheetId="{index}" r:id="rId{index}"/>'
            for index, name in enumerate(sheetNames, start=1)
        )
        return (
            '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
            'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
            f"<sheets>{sheets}</sheets>"
            "</workbook>"
        )

    def _xlsx_workbook_rels(self, sheetCount: int) -> str:
        relationships = []
        for index in range(1, sheetCount + 1):
            relationships.append(
                f'<Relationship Id="rId{index}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet{index}.xml"/>'
            )
        relationships.append(
            f'<Relationship Id="rId{sheetCount + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>'
        )
        relationships.append(
            f'<Relationship Id="rId{sheetCount + 2}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>'
        )
        return (
            '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
            + "".join(relationships)
            + "</Relationships>"
        )

    def _xlsx_styles(self) -> str:
        return (
            '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
            '<fonts count="2">'
            '<font><sz val="11"/><name val="Calibri"/></font>'
            '<font><b/><sz val="11"/><name val="Calibri"/></font>'
            '</fonts>'
            '<fills count="3">'
            '<fill><patternFill patternType="none"/></fill>'
            '<fill><patternFill patternType="gray125"/></fill>'
            '<fill><patternFill patternType="solid"><fgColor rgb="FFD1F41F"/><bgColor indexed="64"/></patternFill></fill>'
            '</fills>'
            '<borders count="2">'
            '<border><left/><right/><top/><bottom/><diagonal/></border>'
            '<border>'
            '<left style="thin"><color auto="1"/></left>'
            '<right style="thin"><color auto="1"/></right>'
            '<top style="thin"><color auto="1"/></top>'
            '<bottom style="thin"><color auto="1"/></bottom>'
            '<diagonal/>'
            '</border>'
            '</borders>'
            '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>'
            '<cellXfs count="2">'
            '<xf numFmtId="0" fontId="0" fillId="0" borderId="1" xfId="0" applyBorder="1"/>'
            '<xf numFmtId="0" fontId="1" fillId="2" borderId="1" xfId="0" applyFont="1" applyFill="1" applyBorder="1" applyAlignment="1">'
            '<alignment vertical="center"/>'
            '</xf>'
            '</cellXfs>'
            '<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>'
            '</styleSheet>'
        )

    def _xlsx_shared_strings(self, values: list[str]) -> str:
        items = "".join(f"<si><t>{escape(value)}</t></si>" for value in values)
        return (
            '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            f'<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="{len(values)}" uniqueCount="{len(values)}">{items}</sst>'
        )

    def _xlsx_sheet(self, headers: list[str], rows: list[list[str]], intern) -> str:
        all_rows: list[list[str]] = []
        if headers:
            all_rows.append(headers)
        all_rows.extend(rows)
        sheet_rows = []
        for rowIndex, row in enumerate(all_rows, start=1):
            cells = []
            styleIndex = "1" if headers and rowIndex == 1 else "0"
            for colIndex, value in enumerate(row, start=1):
                colRef = self._xlsx_column_name(colIndex)
                cells.append(f'<c r="{colRef}{rowIndex}" t="s" s="{styleIndex}"><v>{intern(value)}</v></c>')
            sheet_rows.append(f'<row r="{rowIndex}" ht="26" customHeight="1">{"".join(cells)}</row>')
        return (
            '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
            f'<sheetData>{"".join(sheet_rows)}</sheetData>'
            '</worksheet>'
        )

    def _xlsx_column_name(self, columnIndex: int) -> str:
        result = ""
        current = columnIndex
        while current > 0:
            current, remainder = divmod(current - 1, 26)
            result = chr(65 + remainder) + result
        return result

    def _xlsx_core_props(self) -> str:
        now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
        return (
            '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            '<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" '
            'xmlns:dc="http://purl.org/dc/elements/1.1/" '
            'xmlns:dcterms="http://purl.org/dc/terms/" '
            'xmlns:dcmitype="http://purl.org/dc/dcmitype/" '
            'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
            '<dc:creator>LeAudit</dc:creator>'
            '<cp:lastModifiedBy>LeAudit</cp:lastModifiedBy>'
            f'<dcterms:created xsi:type="dcterms:W3CDTF">{now}</dcterms:created>'
            f'<dcterms:modified xsi:type="dcterms:W3CDTF">{now}</dcterms:modified>'
            '</cp:coreProperties>'
        )

    def _xlsx_app_props(self, sheetNames: list[str]) -> str:
        parts = "".join(f"<vt:lpstr>{escape(name)}</vt:lpstr>" for name in sheetNames)
        return (
            '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            '<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" '
            'xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">'
            '<Application>LeAudit</Application>'
            f'<TitlesOfParts><vt:vector size="{len(sheetNames)}" baseType="lpstr">{parts}</vt:vector></TitlesOfParts>'
            '</Properties>'
        )

    async def _load_table_columns(self, session, tableName: str) -> set[str]:
        rows = (
            await session.execute(
                text(
                    """
                    SELECT column_name
                    FROM information_schema.columns
                    WHERE table_schema = current_schema()
                      AND table_name = :table_name
                    """
                ),
                {"table_name": tableName},
            )
        ).mappings().all()
        return {str(row["column_name"]) for row in rows if row.get("column_name")}
