import json
import re
import uuid
from typing import Any

from yuxi.knowledge import knowledge_base
from yuxi.knowledge.eval.benchmark_generation import (
    dump_benchmark_item,
    iter_generated_benchmark_items,
    normalize_generation_concurrency_count,
)
from yuxi.knowledge.eval.evaluator import aggregate_metrics, evaluate_question
from yuxi.models import select_model
from yuxi.repositories.evaluation_repository import EvaluationRepository
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository
from yuxi.repositories.knowledge_chunk_repository import KnowledgeChunkRepository
from yuxi.repositories.task_repository import TaskRepository
from yuxi.services.task_service import TaskContext, tasker
from yuxi.utils import logger
from yuxi.utils.datetime_utils import format_utc_datetime, utc_now_naive


def build_evaluation_run_name(started_at=None, hash_value: str | None = None) -> str:
    date_part = (started_at or utc_now_naive()).strftime("%Y%m%d")
    hash_part = re.sub(r"[^a-fA-F0-9]", "", hash_value or uuid.uuid4().hex).lower()[:6]
    if len(hash_part) < 6:
        hash_part = (hash_part + uuid.uuid4().hex)[:6]
    return f"eval-{date_part}-{hash_part}"


class EvaluationService:
    """RAG评估服务"""

    def __init__(self):
        self.eval_repo = EvaluationRepository()
        self.kb_repo = KnowledgeBaseRepository()
        self.chunk_repo = KnowledgeChunkRepository()
        self.task_repo = TaskRepository()

    def _dataset_to_dict(self, row) -> dict[str, Any]:
        return {
            "id": row.dataset_id,
            "dataset_id": row.dataset_id,
            "name": row.name,
            "description": row.description,
            "kb_id": row.kb_id,
            "item_count": row.item_count,
            "has_gold_chunks": row.has_gold_chunks,
            "has_gold_answers": row.has_gold_answers,
            "build_metadata": row.build_metadata or {},
            "created_by": row.created_by,
            "created_at": format_utc_datetime(row.created_at),
            "updated_at": format_utc_datetime(row.updated_at),
        }

    def _dataset_item_to_dict(self, item) -> dict[str, Any]:
        return {
            "item_id": item.item_id,
            "item_index": item.item_index,
            "query": item.query_text,
            "gold_chunk_ids": item.gold_chunk_ids or [],
            "gold_answer": item.gold_answer,
        }

    def _run_item_to_dict(self, item) -> dict[str, Any]:
        return {
            "query": item.query_text,
            "gold_chunk_ids": item.gold_chunk_ids,
            "gold_answer": item.gold_answer,
            "generated_answer": item.generated_answer,
            "retrieved_chunks": item.retrieved_chunks,
            "metrics": item.metrics or {},
        }

    def _is_error_run_item(self, item) -> bool:
        metrics = item.metrics or {}
        return metrics.get("score", 1.0) <= 0.5 or any(
            metrics.get(key, 1.0) < 0.3 for key in metrics if key.startswith("recall@")
        )

    def _normalize_run_name(self, name: str | None, run_id: str) -> str:
        run_name = (name or "").strip()
        if run_name:
            return run_name
        return build_evaluation_run_name(hash_value=run_id.removeprefix("run_"))

    def _run_name_from_row(self, row) -> str:
        name = (getattr(row, "name", None) or "").strip()
        if name:
            return name
        return build_evaluation_run_name(row.started_at, hash_value=row.run_id.removeprefix("run_"))

    async def _sync_dataset_build_metadata(self, row) -> None:
        metadata = dict(row.build_metadata or {})
        if metadata.get("source") != "generated" or metadata.get("status") not in {"pending", "running"}:
            return

        task_id = metadata.get("task_id")
        task = await self.task_repo.get_by_id(task_id) if task_id else None
        if task is None:
            metadata.pop("progress", None)
            metadata.update(status="failed", message="生成任务不存在")
        elif task.status == "success":
            metadata.update(status="completed", progress=100, message=task.message or "完成")
        elif task.status in {"failed", "cancelled"}:
            metadata.pop("progress", None)
            metadata.update(status="failed", message=task.error or task.message or "生成任务失败")
        else:
            metadata.update(status=task.status, progress=task.progress, message=task.message)

        if metadata != (row.build_metadata or {}):
            await self.eval_repo.update_dataset(row.dataset_id, {"build_metadata": metadata})
            row.build_metadata = metadata

    def _build_dataset_items(
        self, dataset_id: str, kb_id: str, questions: list[dict[str, Any]]
    ) -> list[dict[str, Any]]:
        return [
            {
                "item_id": f"dataset_item_{uuid.uuid4().hex[:12]}",
                "dataset_id": dataset_id,
                "kb_id": kb_id,
                "item_index": index,
                "query_text": item["query"],
                "gold_chunk_ids": item.get("gold_chunk_ids") or [],
                "gold_answer": item.get("gold_answer"),
            }
            for index, item in enumerate(questions)
        ]

    def _build_jsonl_content(self, items: list[Any]) -> str:
        lines = []
        for item in items:
            payload = {"query": item.query_text}
            if item.gold_chunk_ids:
                payload["gold_chunk_ids"] = item.gold_chunk_ids
            if item.gold_answer:
                payload["gold_answer"] = item.gold_answer
            lines.append(dump_benchmark_item(payload).rstrip("\n"))
        return "\n".join(lines) + ("\n" if lines else "")

    def _safe_jsonl_filename(self, name: str | None, fallback: str) -> str:
        filename = (name or "").strip() or fallback
        filename = re.sub(r"[\\/:*?\"<>|]+", "_", filename).strip()
        if not filename or filename in {".", ".."}:
            filename = fallback
        return filename if filename.endswith(".jsonl") else f"{filename}.jsonl"

    def _parse_jsonl_questions(self, file_content: bytes) -> tuple[list[dict[str, Any]], bool, bool]:
        questions = []
        has_gold_chunks = False
        has_gold_answers = False
        content = file_content.decode("utf-8")

        for line_num, line in enumerate(content.strip().split("\n"), 1):
            if not line.strip():
                continue
            try:
                item = json.loads(line)
            except json.JSONDecodeError as e:
                raise ValueError(f"第{line_num}行JSON格式错误: {str(e)}")
            if "query" not in item:
                raise ValueError(f"第{line_num}行缺少必需的'query'字段")
            if item.get("gold_chunk_ids"):
                has_gold_chunks = True
            if item.get("gold_answer"):
                has_gold_answers = True
            questions.append(item)

        if not questions:
            raise ValueError("文件中没有有效的问题数据")
        return questions, has_gold_chunks, has_gold_answers

    async def upload_dataset(
        self, kb_id: str, file_content: bytes, filename: str, name: str, description: str, created_by: str
    ) -> dict[str, Any]:
        try:
            questions, has_gold_chunks, has_gold_answers = self._parse_jsonl_questions(file_content)
            dataset_id = f"dataset_{uuid.uuid4().hex[:8]}"
            dataset_name = name.strip() or filename or dataset_id

            row = await self.eval_repo.create_dataset_with_items(
                {
                    "dataset_id": dataset_id,
                    "kb_id": kb_id,
                    "name": dataset_name,
                    "description": description,
                    "item_count": len(questions),
                    "has_gold_chunks": has_gold_chunks,
                    "has_gold_answers": has_gold_answers,
                    "build_metadata": {
                        "source": "upload",
                        "status": "completed",
                        "progress": 100,
                        "filename": filename,
                    },
                    "created_by": created_by,
                },
                self._build_dataset_items(dataset_id, kb_id, questions),
            )
            return self._dataset_to_dict(row)
        except Exception as e:
            logger.error(f"上传评估数据集失败: {e}")
            raise

    async def list_datasets(self, kb_id: str) -> list[dict[str, Any]]:
        try:
            rows = await self.eval_repo.list_datasets(kb_id)
            for row in rows:
                await self._sync_dataset_build_metadata(row)
            return [self._dataset_to_dict(row) for row in rows]
        except Exception as e:
            logger.error(f"获取评估数据集列表失败: {e}")
            raise

    async def get_dataset_detail(
        self, kb_id: str, dataset_id: str, page: int = 1, page_size: int = 10
    ) -> dict[str, Any]:
        try:
            row = await self.eval_repo.get_dataset(dataset_id)
            if row is None or row.kb_id != kb_id:
                raise ValueError("Dataset not found")
            if (row.build_metadata or {}).get("status", "completed") != "completed":
                raise ValueError("Dataset is not ready")

            total_items = await self.eval_repo.count_dataset_items(dataset_id)
            items = await self.eval_repo.list_dataset_items(dataset_id, (page - 1) * page_size, page_size)
            total_pages = (total_items + page_size - 1) // page_size
            data = self._dataset_to_dict(row)
            data.update(
                {
                    "items": [self._dataset_item_to_dict(item) for item in items],
                    "pagination": {
                        "current_page": page,
                        "page_size": page_size,
                        "total_items": total_items,
                        "total_pages": total_pages,
                        "has_next": page < total_pages,
                        "has_prev": page > 1,
                    },
                }
            )
            return data
        except Exception as e:
            logger.error(f"获取评估数据集详情失败: {e}")
            raise

    async def export_dataset_jsonl(self, dataset_id: str) -> dict[str, str]:
        row = await self.eval_repo.get_dataset(dataset_id)
        if row is None:
            raise ValueError("Dataset not found")
        if (row.build_metadata or {}).get("status", "completed") != "completed":
            raise ValueError("Dataset is not ready")
        items = await self.eval_repo.list_all_dataset_items(dataset_id)
        return {
            "filename": self._safe_jsonl_filename(row.name, row.dataset_id),
            "content": self._build_jsonl_content(items),
        }

    async def delete_dataset(self, dataset_id: str) -> None:
        try:
            row = await self.eval_repo.get_dataset(dataset_id)
            if row is None:
                raise ValueError("Dataset not found")
            await self.eval_repo.delete_dataset(dataset_id)
            logger.info(f"成功删除评估数据集: {dataset_id}")
        except Exception as e:
            logger.error(f"删除评估数据集失败: {e}")
            raise

    async def generate_dataset(
        self,
        kb_id: str,
        name: str,
        description: str,
        count: int,
        neighbors_count: int,
        concurrency_count: int,
        llm_model_spec: str,
        generation_mode: str = "vector",
        graph_expand_top_k: int = 1,
        created_by: str = "system",
    ) -> dict[str, Any]:
        dataset_id = f"dataset_{uuid.uuid4().hex[:8]}"
        count = int(count)
        neighbors_count = int(neighbors_count)
        concurrency_count = normalize_generation_concurrency_count(concurrency_count)
        graph_expand_top_k = min(max(1, int(graph_expand_top_k)), 3)
        if generation_mode not in {"vector", "graph_enhanced"}:
            raise ValueError("不支持的评估基准生成方式")
        if generation_mode == "graph_enhanced":
            indexed_count = await self.chunk_repo.count_graph_indexed_by_kb_id(kb_id)
            if indexed_count <= 0:
                raise ValueError("当前知识库尚未完成图索引，无法使用图增强构建")
        build_metadata = {
            "source": "generated",
            "status": "pending",
            "progress": 0,
            "params": {
                "count": count,
                "neighbors_count": neighbors_count,
                "concurrency_count": concurrency_count,
                "llm_model_spec": llm_model_spec,
                "generation_mode": generation_mode,
                "graph_expand_top_k": graph_expand_top_k,
            },
        }
        await self.eval_repo.create_dataset(
            {
                "dataset_id": dataset_id,
                "kb_id": kb_id,
                "name": name,
                "description": description,
                "item_count": 0,
                "has_gold_chunks": True,
                "has_gold_answers": True,
                "build_metadata": build_metadata,
                "created_by": created_by,
            }
        )
        task = await tasker.enqueue(
            name="生成评估数据集",
            task_type="dataset_generation",
            payload={
                "dataset_id": dataset_id,
                "kb_id": kb_id,
                "created_by": created_by,
                "name": name,
                "description": description,
                "count": count,
                "neighbors_count": neighbors_count,
                "concurrency_count": concurrency_count,
                "llm_model_spec": llm_model_spec,
                "generation_mode": generation_mode,
                "graph_expand_top_k": graph_expand_top_k,
            },
            coroutine=self._generate_dataset_task,
        )
        build_metadata["task_id"] = task.id
        await self.eval_repo.update_dataset(dataset_id, {"build_metadata": build_metadata})
        return {"dataset_id": dataset_id, "task_id": task.id, "message": "评估数据集生成任务已提交"}

    async def _update_dataset_build_metadata(
        self, dataset_id: str, metadata: dict[str, Any], **updates
    ) -> dict[str, Any]:
        metadata.update(updates)
        await self.eval_repo.update_dataset(dataset_id, {"build_metadata": metadata})
        return metadata

    async def _generate_dataset_task(self, context: TaskContext):
        await context.set_progress(0, "初始化")
        task = context._tasker._tasks.get(context.task_id)
        payload = task.payload if task else {}

        dataset_id = payload.get("dataset_id")
        kb_id = payload.get("kb_id")
        count = int(payload.get("count", 10))
        neighbors_count = int(payload.get("neighbors_count", 1))
        concurrency_count = normalize_generation_concurrency_count(payload.get("concurrency_count"))
        llm_model_spec = payload.get("llm_model_spec")
        generation_mode = payload.get("generation_mode") or "vector"
        graph_expand_top_k = min(max(1, int(payload.get("graph_expand_top_k", 1))), 3)
        build_metadata = {
            "source": "generated",
            "status": "running",
            "progress": 0,
            "task_id": context.task_id,
            "params": {
                "count": count,
                "neighbors_count": neighbors_count,
                "concurrency_count": concurrency_count,
                "llm_model_spec": llm_model_spec,
                "generation_mode": generation_mode,
                "graph_expand_top_k": graph_expand_top_k,
            },
        }
        await self._update_dataset_build_metadata(dataset_id, build_metadata)

        async def report_progress(progress: float, message: str | None = None) -> None:
            await context.set_progress(progress, message)
            await self._update_dataset_build_metadata(
                dataset_id,
                build_metadata,
                progress=max(0, min(round(progress), 100)),
                message=message or build_metadata.get("message", ""),
            )

        try:
            kb_instance = await knowledge_base.aget_kb(kb_id)
            if not kb_instance:
                await report_progress(100, "知识库不存在")
                raise ValueError("Knowledge Base not found")
            if kb_instance.kb_type != "milvus":
                await report_progress(100, "仅支持 commonrag/Milvus 类型知识库生成评估数据集")
                raise ValueError("Unsupported KB type for dataset generation")

            questions = []
            try:
                async for item in iter_generated_benchmark_items(
                    kb_instance=kb_instance,
                    kb_id=kb_id,
                    count=count,
                    neighbors_count=neighbors_count,
                    llm_model_spec=llm_model_spec,
                    concurrency_count=concurrency_count,
                    generation_mode=generation_mode,
                    graph_expand_top_k=graph_expand_top_k,
                    progress_cb=report_progress,
                    cancel_cb=context.raise_if_cancelled,
                ):
                    questions.append(item)
            except ValueError as e:
                if str(e) == "No chunks found in knowledge base":
                    await report_progress(100, "知识库为空或未解析到chunks")
                raise

            if not questions:
                raise ValueError("未生成有效评估题目")

            await self.eval_repo.add_dataset_items(self._build_dataset_items(dataset_id, kb_id, questions))
            await self.eval_repo.update_dataset(dataset_id, {"item_count": len(questions)})
            await self._update_dataset_build_metadata(
                dataset_id,
                build_metadata,
                status="completed",
                progress=100,
                message="完成",
            )
            await context.set_progress(100, "完成")
        except Exception as e:
            await self._update_dataset_build_metadata(
                dataset_id,
                build_metadata,
                status="failed",
                progress=100,
                error_message=str(e),
                message=str(e),
            )
            raise

    async def run_evaluation(
        self,
        kb_id: str,
        dataset_id: str,
        name: str | None = None,
        model_config: dict[str, Any] = None,
        created_by: str = "system",
    ) -> str:
        try:
            run_id = f"run_{uuid.uuid4().hex[:8]}"
            run_name = self._normalize_run_name(name, run_id)
            dataset_row = await self.eval_repo.get_dataset(dataset_id)
            if dataset_row is None or dataset_row.kb_id != kb_id:
                raise ValueError("Dataset not found")
            if (dataset_row.build_metadata or {}).get("status", "completed") != "completed":
                raise ValueError("Dataset is not ready")

            retrieval_config = {}
            try:
                kb_row = await self.kb_repo.get_by_kb_id(kb_id)
                query_params = (kb_row.query_params if kb_row else None) or {}
                retrieval_config = query_params.get("options", {}) if isinstance(query_params, dict) else {}
                if not retrieval_config:
                    kb_instance = await knowledge_base.aget_kb(kb_id)
                    if kb_instance:
                        retrieval_config = kb_instance._get_default_query_params(kb_id).get("options", {})
                logger.info(f"从知识库 {kb_id} 加载检索配置: {list(retrieval_config.keys())}")
            except Exception as e:
                logger.error(f"获取知识库检索配置失败: {e}")

            if model_config:
                retrieval_config.update(model_config)

            await self.eval_repo.create_run(
                {
                    "run_id": run_id,
                    "name": run_name,
                    "kb_id": kb_id,
                    "dataset_id": dataset_id,
                    "status": "running",
                    "retrieval_config": retrieval_config,
                    "metrics": {},
                    "overall_score": None,
                    "total_items": dataset_row.item_count or 0,
                    "completed_items": 0,
                    "started_at": utc_now_naive(),
                    "completed_at": None,
                    "created_by": created_by,
                }
            )

            await tasker.enqueue(
                name=f"RAG评估({run_name})",
                task_type="rag_evaluation",
                payload={
                    "run_id": run_id,
                    "name": run_name,
                    "kb_id": kb_id,
                    "dataset_id": dataset_id,
                    "retrieval_config": retrieval_config,
                    "created_by": created_by,
                },
                coroutine=self._run_evaluation_task,
            )
            return run_id
        except Exception as e:
            logger.error(f"启动评估失败: {e}")
            raise

    async def _run_evaluation_task(self, context: TaskContext):
        try:
            task = context._tasker._tasks.get(context.task_id)
            if not task:
                raise ValueError("Task not found")
            payload = task.payload

            run_id = payload["run_id"]
            kb_id = payload["kb_id"]
            dataset_id = payload["dataset_id"]
            retrieval_config = payload["retrieval_config"]

            await context.set_progress(5, "加载评估数据集")
            dataset_row = await self.eval_repo.get_dataset(dataset_id)
            if dataset_row is None or dataset_row.kb_id != kb_id:
                raise ValueError("Dataset not found")
            dataset_items = await self.eval_repo.list_all_dataset_items(dataset_id)
            if not dataset_items:
                raise ValueError("Dataset has no items")

            kb_instance = await knowledge_base.aget_kb(kb_id)
            if not kb_instance:
                raise ValueError(f"Knowledge Base {kb_id} not found")

            judge_llm = None
            if dataset_row.has_gold_answers:
                judge_model_spec = retrieval_config.get("judge_llm") or retrieval_config.get("answer_llm")
                if judge_model_spec:
                    try:
                        logger.debug(f"Initializing Judge LLM: {judge_model_spec}")
                        judge_llm = select_model(model_spec=judge_model_spec)
                    except Exception as e:
                        logger.error(f"Failed to load judge LLM: {e}")

            all_retrieval_metrics = []
            all_answer_metrics = []
            total_items = len(dataset_items)

            async def update_run_db(status=None, completed=None, metrics=None, final_score=None):
                data = {}
                if status is not None:
                    data["status"] = status
                    if status in ["completed", "failed"]:
                        data["completed_at"] = utc_now_naive()
                if completed is not None:
                    data["completed_items"] = completed
                if metrics is not None:
                    data["metrics"] = metrics
                if final_score is not None:
                    data["overall_score"] = final_score
                if data:
                    await self.eval_repo.update_run(run_id, data)

            for index, item in enumerate(dataset_items):
                await context.raise_if_cancelled()
                progress = 10 + (index / total_items) * 80
                await context.set_progress(progress, f"评估 {index + 1}/{total_items}")

                question_data = {
                    "query": item.query_text,
                    "gold_chunk_ids": item.gold_chunk_ids or [],
                    "gold_answer": item.gold_answer,
                }
                question_result = await evaluate_question(
                    kb_instance=kb_instance,
                    kb_id=kb_id,
                    question_data=question_data,
                    retrieval_config=retrieval_config,
                    has_gold_chunks=dataset_row.has_gold_chunks,
                    has_gold_answers=dataset_row.has_gold_answers,
                    judge_llm=judge_llm,
                    select_model_fn=select_model,
                )

                if dataset_row.has_gold_chunks and question_data.get("gold_chunk_ids"):
                    all_retrieval_metrics.append(question_result["retrieval_scores"])
                if dataset_row.has_gold_answers and question_data.get("gold_answer") and judge_llm:
                    all_answer_metrics.append(question_result["answer_scores"])

                await self.eval_repo.upsert_run_item(
                    run_id=run_id,
                    item_index=index,
                    data={"dataset_item_id": item.item_id, **question_result["detail"]},
                )

                if (index + 1) % 5 == 0 or (index + 1) == total_items:
                    current_metrics, _ = aggregate_metrics(all_retrieval_metrics, all_answer_metrics)
                    await context.set_result(
                        {"current_metrics": current_metrics, "completed_items": index + 1, "total_items": total_items}
                    )
                    await update_run_db(completed=index + 1)

            await context.set_progress(95, "计算最终指标")
            overall_metrics, overall_score = aggregate_metrics(
                all_retrieval_metrics, all_answer_metrics, include_overall_score=True
            )
            await update_run_db(
                status="completed",
                completed=total_items,
                metrics=overall_metrics,
                final_score=overall_score,
            )
            await context.set_progress(100, "完成")
        except Exception as e:
            logger.error(f"Task failed: {e}")
            try:
                if "payload" in locals():
                    await self.eval_repo.update_run(
                        payload["run_id"],
                        {"status": "failed", "metrics": {"error": str(e)}, "completed_at": utc_now_naive()},
                    )
            except Exception as exc:
                logger.error(f"Error updating run record: {exc}")
            await context.set_message(f"Error: {str(e)}")
            raise

    async def list_runs(self, kb_id: str) -> list[dict[str, Any]]:
        try:
            rows = await self.eval_repo.list_runs(kb_id)
            running_run_ids = {row.run_id for row in rows if row.status == "running"}
            task_by_run_id = {}
            if running_run_ids:
                tasks = await self.task_repo.list_all()
                task_by_run_id = {
                    (task.payload or {}).get("run_id"): task
                    for task in tasks
                    if task.type == "rag_evaluation"
                    and task.status in {"pending", "running"}
                    and (task.payload or {}).get("run_id") in running_run_ids
                }

            runs = []
            for row in rows:
                run = {
                    "run_id": row.run_id,
                    "name": self._run_name_from_row(row),
                    "dataset_id": row.dataset_id,
                    "status": row.status,
                    "started_at": format_utc_datetime(row.started_at),
                    "completed_at": format_utc_datetime(row.completed_at),
                    "total_items": row.total_items,
                    "completed_items": row.completed_items,
                    "overall_score": row.overall_score,
                    "retrieval_config": row.retrieval_config or {},
                    "metrics": row.metrics or {},
                }
                if row.status == "running":
                    task = task_by_run_id.get(row.run_id)
                    if task:
                        run.update(progress=task.progress, message=task.message)
                runs.append(run)
            return runs
        except Exception as e:
            logger.error(f"获取评估运行历史失败: {e}")
            raise

    async def get_run_results(
        self, kb_id: str, run_id: str, page: int = 1, page_size: int = 20, error_only: bool = False
    ) -> dict[str, Any]:
        if not re.match(r"^run_[a-f0-9]{8}$", run_id):
            raise ValueError("Invalid run_id format")
        row = await self.eval_repo.get_run(run_id)
        if row is None or row.kb_id != kb_id:
            task = await tasker.get_task(run_id)
            if task:
                return {"run_id": run_id, "status": task.status, "progress": task.progress, "message": task.message}
            raise ValueError(f"Run not found for {run_id}")

        start_idx = (page - 1) * page_size
        if error_only:
            total = 0
            paged_items = []
            offset = 0
            batch_size = 200
            while True:
                batch = await self.eval_repo.list_run_items(run_id, offset, batch_size)
                if not batch:
                    break
                for item in batch:
                    if not self._is_error_run_item(item):
                        continue
                    if start_idx <= total < start_idx + page_size:
                        paged_items.append(self._run_item_to_dict(item))
                    total += 1
                offset += batch_size
        else:
            total = await self.eval_repo.count_run_items(run_id)
            details = await self.eval_repo.list_run_items(run_id, start_idx, page_size)
            paged_items = [self._run_item_to_dict(item) for item in details]
        return {
            "run_id": row.run_id,
            "name": self._run_name_from_row(row),
            "status": row.status,
            "started_at": format_utc_datetime(row.started_at),
            "completed_at": format_utc_datetime(row.completed_at),
            "total_items": row.total_items or 0,
            "completed_items": row.completed_items or 0,
            "overall_score": row.overall_score,
            "retrieval_config": row.retrieval_config or {},
            "items": paged_items,
            "pagination": {
                "current_page": page,
                "page_size": page_size,
                "total": total,
                "total_pages": (total + page_size - 1) // page_size,
                "error_only": error_only,
            },
        }

    async def delete_run(self, kb_id: str, run_id: str) -> None:
        if not re.match(r"^run_[a-f0-9]{8}$", run_id):
            raise ValueError("Invalid run_id format")
        row = await self.eval_repo.get_run(run_id)
        if row is None or row.kb_id != kb_id:
            raise ValueError("Run not found")
        await self.eval_repo.delete_run(run_id)
        logger.info(f"成功删除评估运行: {run_id}")
