from __future__ import annotations

import json

from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import and_, func, literal, or_, select, union_all
from sqlalchemy.orm import Session

from app.core.database import get_db
from app.core.dependencies import get_current_admin
from app.core.responses import api_success
from app.api.pagination import page_result
from app.models.admin import Admin
from app.models.knowledge import (
    Knowledge,
    KnowledgeCard,
    KnowledgeChunk,
    KnowledgeManifest,
    KnowledgeSection,
    KnowledgeSyncJob,
    KnowledgeVersion,
)
from app.schemas.knowledge import (
    KnowledgeBatchMetadataUpdateRequest,
    KnowledgeBatchSyncRequest,
    KnowledgeLifecycleRequest,
    KnowledgeMetadataUpdateRequest,
    KnowledgeStatusRequest,
)
from app.services.admin_service import OperationLogService
from app.services.knowledge_pipeline_service import KnowledgePipelineService

router = APIRouter()


@router.patch("/knowledge/batch/metadata")
def batch_update_metadata(
    payload: KnowledgeBatchMetadataUpdateRequest,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    _require_super_admin(current_admin)
    if payload.knowledgeType is None:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请选择待修改的知识库类型")
    knowledge_ids = list(dict.fromkeys(payload.knowledgeIds))
    rows = {
        item.id: item
        for item in db.scalars(select(Knowledge).where(Knowledge.id.in_(knowledge_ids))).all()
    }
    results: list[dict] = []
    for knowledge_id in knowledge_ids:
        item = rows.get(knowledge_id)
        if item is None:
            results.append({"knowledgeId": knowledge_id, "ok": False, "error": "知识库不存在"})
            continue
        if payload.knowledgeType is not None:
            item.knowledge_type = payload.knowledgeType
        db.add(item)
        OperationLogService.write(
            db,
            admin_id=current_admin.id,
            module="knowledge",
            action="batch_update_metadata",
            target_id=item.id,
        )
        results.append({"knowledgeId": knowledge_id, "ok": True})
    db.commit()
    success = sum(1 for item in results if item["ok"])
    return api_success(
        {"total": len(results), "success": success, "failed": len(results) - success, "results": results}
    )


@router.patch("/knowledge/{knowledge_id}/metadata")
def update_metadata(
    knowledge_id: int,
    payload: KnowledgeMetadataUpdateRequest,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    _require_super_admin(current_admin)
    knowledge = _knowledge(db, knowledge_id)
    knowledge.name = payload.name.strip()
    knowledge.knowledge_type = payload.knowledgeType
    knowledge.review_mode = "automatic"
    db.add(knowledge)
    OperationLogService.write(
        db, admin_id=current_admin.id, module="knowledge", action="update_metadata", target_id=knowledge.id
    )
    db.commit()
    db.refresh(knowledge)
    return api_success(_knowledge_detail(knowledge))


@router.get("/knowledge/{knowledge_id}/detail")
def detail(
    knowledge_id: int,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    _require_super_admin(current_admin)
    knowledge = _knowledge(db, knowledge_id)
    current = db.get(KnowledgeVersion, knowledge.current_version_id) if knowledge.current_version_id else None
    manifest = None
    manifest_version_id = knowledge.current_version_id
    if manifest_version_id:
        manifest = db.scalar(select(KnowledgeManifest).where(KnowledgeManifest.version_id == manifest_version_id))
    return api_success(
        {
            "knowledge": _knowledge_detail(knowledge),
            "currentVersion": _version_dict(current),
            "manifest": _manifest_dict(manifest),
        }
    )


@router.get("/knowledge/version/{version_id}/content")
def version_content(
    version_id: int,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    _require_super_admin(current_admin)
    version = _version(db, version_id)
    sections = db.scalars(
        select(KnowledgeSection).where(KnowledgeSection.version_id == version.id).order_by(KnowledgeSection.sort_order)
    ).all()
    cards = db.scalars(
        select(KnowledgeCard).where(KnowledgeCard.version_id == version.id).order_by(KnowledgeCard.id)
    ).all()
    manifest = db.scalar(select(KnowledgeManifest).where(KnowledgeManifest.version_id == version.id))
    return api_success(
        {
            "version": _version_dict(version),
            "manifest": _manifest_dict(manifest),
            "sections": [
                {
                    "id": item.id,
                    "sectionKey": item.section_key,
                    "title": item.title,
                    "content": item.content,
                    "sourceStart": item.source_start,
                    "sourceEnd": item.source_end,
                    "contentHash": item.content_hash,
                }
                for item in sections
            ],
            "cards": [
                {
                    "id": item.id,
                    "sectionId": item.section_id,
                    "title": item.title,
                    "summary": item.summary,
                    "coreConclusion": item.core_conclusion,
                    "applicableQuestions": item.applicable_questions,
                    "inapplicableQuestions": item.inapplicable_questions,
                    "keywords": _json_value(item.keywords),
                    "synonyms": _json_value(item.synonyms),
                    "riskLevel": item.risk_level,
                    "sourceRange": item.source_range,
                    "reviewStatus": item.review_status,
                }
                for item in cards
            ],
        }
    )


@router.get("/knowledge/content-search")
def content_search(
    keyword: str = Query(default="", min_length=1, max_length=100),
    includeClosed: bool = Query(default=True),
    page: int = Query(default=1, ge=1),
    pageSize: int = Query(default=10, ge=5, le=50),
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    _require_super_admin(current_admin)
    keyword = keyword.strip()
    if not keyword:
        return api_success({"items": [], "total": 0, "page": page, "pageSize": pageSize})

    pattern = f"%{_escape_like(keyword)}%"
    base_filters = [
        Knowledge.current_version_id.is_not(None),
        Knowledge.current_version_id == KnowledgeVersion.id,
        Knowledge.lifecycle_status == "active",
    ]
    if not includeClosed:
        base_filters.append(Knowledge.status == 1)

    section_query = (
        select(
            Knowledge.id.label("knowledge_id"),
            Knowledge.name.label("knowledge_name"),
            Knowledge.source_title.label("source_title"),
            Knowledge.knowledge_type.label("knowledge_type"),
            Knowledge.status.label("knowledge_status"),
            Knowledge.lifecycle_status.label("lifecycle_status"),
            Knowledge.source_status.label("source_status"),
            KnowledgeVersion.id.label("version_id"),
            KnowledgeVersion.version_no.label("version_no"),
            KnowledgeVersion.published_at.label("published_at"),
            literal("section").label("item_type"),
            literal(1).label("item_rank"),
            KnowledgeSection.id.label("item_id"),
            KnowledgeSection.id.label("section_id"),
            KnowledgeSection.section_key.label("section_key"),
            KnowledgeSection.title.label("title"),
            KnowledgeSection.content.label("content"),
            KnowledgeSection.sort_order.label("sort_order"),
        )
        .select_from(KnowledgeSection)
        .join(Knowledge, Knowledge.id == KnowledgeSection.knowledge_id)
        .join(KnowledgeVersion, KnowledgeVersion.id == KnowledgeSection.version_id)
        .where(*base_filters)
        .where(or_(KnowledgeSection.title.like(pattern, escape="\\"), KnowledgeSection.content.like(pattern, escape="\\")))
    )
    card_query = (
        select(
            Knowledge.id.label("knowledge_id"),
            Knowledge.name.label("knowledge_name"),
            Knowledge.source_title.label("source_title"),
            Knowledge.knowledge_type.label("knowledge_type"),
            Knowledge.status.label("knowledge_status"),
            Knowledge.lifecycle_status.label("lifecycle_status"),
            Knowledge.source_status.label("source_status"),
            KnowledgeVersion.id.label("version_id"),
            KnowledgeVersion.version_no.label("version_no"),
            KnowledgeVersion.published_at.label("published_at"),
            literal("card").label("item_type"),
            literal(2).label("item_rank"),
            KnowledgeCard.id.label("item_id"),
            KnowledgeCard.section_id.label("section_id"),
            KnowledgeSection.section_key.label("section_key"),
            KnowledgeCard.title.label("title"),
            KnowledgeCard.summary.label("content"),
            KnowledgeSection.sort_order.label("sort_order"),
        )
        .select_from(KnowledgeCard)
        .join(Knowledge, Knowledge.id == KnowledgeCard.knowledge_id)
        .join(KnowledgeVersion, KnowledgeVersion.id == KnowledgeCard.version_id)
        .join(KnowledgeSection, KnowledgeSection.id == KnowledgeCard.section_id)
        .where(*base_filters)
        .where(
            or_(
                KnowledgeCard.title.like(pattern, escape="\\"),
                KnowledgeCard.summary.like(pattern, escape="\\"),
                KnowledgeCard.core_conclusion.like(pattern, escape="\\"),
                KnowledgeCard.applicable_questions.like(pattern, escape="\\"),
                KnowledgeCard.keywords.like(pattern, escape="\\"),
                KnowledgeCard.synonyms.like(pattern, escape="\\"),
            )
        )
    )
    chunk_query = (
        select(
            Knowledge.id.label("knowledge_id"),
            Knowledge.name.label("knowledge_name"),
            Knowledge.source_title.label("source_title"),
            Knowledge.knowledge_type.label("knowledge_type"),
            Knowledge.status.label("knowledge_status"),
            Knowledge.lifecycle_status.label("lifecycle_status"),
            Knowledge.source_status.label("source_status"),
            KnowledgeVersion.id.label("version_id"),
            KnowledgeVersion.version_no.label("version_no"),
            KnowledgeVersion.published_at.label("published_at"),
            literal("chunk").label("item_type"),
            literal(3).label("item_rank"),
            KnowledgeChunk.id.label("item_id"),
            KnowledgeChunk.section_id.label("section_id"),
            KnowledgeSection.section_key.label("section_key"),
            KnowledgeChunk.title.label("title"),
            KnowledgeChunk.content.label("content"),
            KnowledgeSection.sort_order.label("sort_order"),
        )
        .select_from(KnowledgeChunk)
        .join(Knowledge, Knowledge.id == KnowledgeChunk.knowledge_id)
        .join(KnowledgeVersion, KnowledgeVersion.id == KnowledgeChunk.version_id)
        .join(KnowledgeSection, KnowledgeSection.id == KnowledgeChunk.section_id)
        .where(*base_filters)
        .where(
            or_(
                KnowledgeChunk.title.like(pattern, escape="\\"),
                KnowledgeChunk.content.like(pattern, escape="\\"),
                KnowledgeChunk.normalized_text.like(pattern, escape="\\"),
                KnowledgeChunk.keywords.like(pattern, escape="\\"),
                KnowledgeChunk.synonyms.like(pattern, escape="\\"),
            )
        )
    )
    search = union_all(section_query, card_query, chunk_query).subquery()
    grouped = (
        select(
            search.c.knowledge_id,
            search.c.version_id,
            search.c.section_id,
            func.max(search.c.knowledge_status).label("knowledge_status"),
            func.min(search.c.sort_order).label("sort_order"),
            func.min(search.c.item_rank).label("item_rank"),
            func.min(search.c.item_id).label("item_id"),
        )
        .group_by(search.c.knowledge_id, search.c.version_id, search.c.section_id)
        .subquery()
    )
    total = db.scalar(select(func.count()).select_from(grouped)) or 0
    page_groups = (
        select(grouped.c.knowledge_id, grouped.c.version_id, grouped.c.section_id)
        .order_by(
            grouped.c.knowledge_status.desc(),
            grouped.c.knowledge_id.desc(),
            grouped.c.sort_order.asc(),
            grouped.c.item_rank.asc(),
            grouped.c.item_id.asc(),
        )
        .offset((page - 1) * pageSize)
        .limit(pageSize)
        .subquery()
    )
    rows = db.execute(
        select(search)
        .join(
            page_groups,
            and_(
                page_groups.c.knowledge_id == search.c.knowledge_id,
                page_groups.c.version_id == search.c.version_id,
                page_groups.c.section_id == search.c.section_id,
            ),
        )
        .order_by(
            search.c.knowledge_status.desc(),
            search.c.knowledge_id.desc(),
            search.c.sort_order.asc(),
            search.c.item_rank.asc(),
            search.c.item_id.asc(),
        )
    ).mappings().all()
    section_ids = [int(row["section_id"]) for row in rows if row["section_id"]]
    chunk_section_ids = set()
    if section_ids:
        chunk_section_ids = set(
            db.scalars(select(KnowledgeChunk.section_id).where(KnowledgeChunk.section_id.in_(section_ids))).all()
        )
    merged = _merge_content_search_rows(rows, keyword, chunk_section_ids)
    return api_success(page_result(merged, total=total, page=page, page_size=pageSize))


@router.get("/knowledge/{knowledge_id}/sync-jobs")
def sync_jobs(
    knowledge_id: int,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    _require_super_admin(current_admin)
    rows = db.scalars(
        select(KnowledgeSyncJob)
        .where(KnowledgeSyncJob.knowledge_id == knowledge_id)
        .order_by(KnowledgeSyncJob.id.desc())
        .limit(50)
    ).all()
    return api_success([_job_dict(item) for item in rows])


@router.post("/knowledge/batch/sync")
async def batch_sync(
    payload: KnowledgeBatchSyncRequest,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    _require_super_admin(current_admin)
    knowledge_ids = list(dict.fromkeys(payload.knowledgeIds))
    rows = {
        item.id: item
        for item in db.scalars(select(Knowledge).where(Knowledge.id.in_(knowledge_ids))).all()
    }
    results: list[dict] = []
    for knowledge_id in knowledge_ids:
        knowledge = rows.get(knowledge_id)
        if knowledge is None:
            results.append({"knowledgeId": knowledge_id, "ok": False, "error": "知识库不存在"})
            continue
        if knowledge.lifecycle_status == "archived":
            results.append(
                {
                    "knowledgeId": knowledge_id,
                    "name": knowledge.name,
                    "ok": False,
                    "error": "归档知识库不能同步",
                }
            )
            continue
        try:
            job = await KnowledgePipelineService.synchronize(
                db, knowledge, admin_id=current_admin.id
            )
            OperationLogService.write(
                db,
                admin_id=current_admin.id,
                module="knowledge",
                action="batch_sync",
                target_id=knowledge.id,
            )
            db.commit()
            results.append(
                {
                    "knowledgeId": knowledge.id,
                    "name": knowledge.name,
                    "ok": True,
                    "status": job.status,
                    "job": _job_dict(job),
                }
            )
        except Exception as exc:
            OperationLogService.write(
                db,
                admin_id=current_admin.id,
                module="knowledge",
                action="batch_sync",
                target_id=knowledge.id,
                result="FAILED",
            )
            db.commit()
            results.append(
                {
                    "knowledgeId": knowledge.id,
                    "name": knowledge.name,
                    "ok": False,
                    "error": str(exc),
                }
            )
    success = sum(1 for item in results if item["ok"])
    return api_success(
        {"total": len(results), "success": success, "failed": len(results) - success, "results": results}
    )


@router.post("/knowledge/{knowledge_id}/sync")
async def synchronize(
    knowledge_id: int,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    _require_super_admin(current_admin)
    knowledge = _knowledge(db, knowledge_id)
    if knowledge.lifecycle_status == "archived":
        raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="归档知识库不能同步")
    try:
        job = await KnowledgePipelineService.synchronize(
            db, knowledge, admin_id=current_admin.id
        )
    except Exception as exc:
        raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"知识同步失败：{exc}") from exc
    OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="sync", target_id=knowledge.id)
    db.commit()
    return api_success(_job_dict(job))


@router.put("/knowledge/{knowledge_id}/open-status")
def open_status(
    knowledge_id: int,
    payload: KnowledgeStatusRequest,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    _require_super_admin(current_admin)
    knowledge = _knowledge(db, knowledge_id)
    if payload.open:
        if not knowledge.current_version_id:
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="知识库没有正式发布版本")
        if knowledge.source_status != "normal":
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="飞书源文章异常，不能开放")
        if knowledge.lifecycle_status != "active":
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="归档知识库不能开放")
    knowledge.status = 1 if payload.open else 0
    db.add(knowledge)
    OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="open" if payload.open else "close", target_id=knowledge.id)
    db.commit()
    return api_success(_knowledge_detail(knowledge))


@router.put("/knowledge/{knowledge_id}/lifecycle")
def lifecycle(
    knowledge_id: int,
    payload: KnowledgeLifecycleRequest,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    _require_super_admin(current_admin)
    knowledge = _knowledge(db, knowledge_id)
    if knowledge.lifecycle_status == payload.status:
        return api_success(_knowledge_detail(knowledge))
    knowledge.lifecycle_status = payload.status
    if payload.status == "archived":
        knowledge.status = 0
    db.add(knowledge)
    OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action=payload.status, target_id=knowledge.id)
    db.commit()
    return api_success(_knowledge_detail(knowledge))


@router.get("/knowledge-migration/summary")
def migration_summary(
    db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)
) -> dict:
    _require_super_admin(current_admin)
    rows = db.execute(
        select(Knowledge.migration_status, func.count(Knowledge.id)).group_by(Knowledge.migration_status)
    ).all()
    counts = {migration_status: count for migration_status, count in rows}
    return api_success({"total": sum(counts.values()), "counts": counts})


@router.post("/knowledge-migration/run")
async def run_migration(
    knowledgeId: int | None = Query(default=None),
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    _require_super_admin(current_admin)
    query = select(Knowledge).where(Knowledge.lifecycle_status == "active")
    if knowledgeId is not None:
        query = query.where(Knowledge.id == knowledgeId)
    rows = db.scalars(query.order_by(Knowledge.id)).all()
    results = []
    for knowledge in rows:
        try:
            job = await KnowledgePipelineService.synchronize(
                db, knowledge, admin_id=current_admin.id
            )
            results.append({"knowledgeId": knowledge.id, "ok": True, "job": _job_dict(job)})
        except Exception as exc:
            results.append({"knowledgeId": knowledge.id, "ok": False, "error": str(exc)})
    OperationLogService.write(db, admin_id=current_admin.id, module="knowledge_migration", action="run", target_id=knowledgeId)
    db.commit()
    return api_success({"total": len(results), "success": sum(1 for item in results if item["ok"]), "results": results})


def _require_super_admin(admin: Admin) -> None:
    if admin.role is not None and admin.role.code != "super_admin":
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="仅超级管理员可以执行此操作")


def _knowledge(db: Session, knowledge_id: int) -> Knowledge:
    item = db.get(Knowledge, knowledge_id)
    if item is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="知识库不存在")
    return item


def _version(db: Session, version_id: int) -> KnowledgeVersion:
    item = db.get(KnowledgeVersion, version_id)
    if item is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="知识版本不存在")
    return item


def _knowledge_detail(item: Knowledge) -> dict:
    return {
        "id": item.id,
        "name": item.name,
        "status": item.status,
        "remark": item.remark,
        "sourceTitle": item.source_title,
        "feishuSpaceId": item.feishu_space_id,
        "feishuNodeId": item.feishu_node_id,
        "knowledgeType": item.knowledge_type,
        "open": item.status == 1,
        "lifecycleStatus": item.lifecycle_status,
        "sourceStatus": item.source_status,
        "sourceError": item.source_error,
        "currentVersionId": item.current_version_id,
        "lastSyncedAt": item.last_synced_at,
        "lastPublishedAt": item.last_published_at,
        "migrationStatus": item.migration_status,
        "createdAt": item.created_at,
        "updatedAt": item.updated_at,
    }


def _version_dict(item: KnowledgeVersion | None) -> dict | None:
    if item is None:
        return None
    return {
        "id": item.id,
        "knowledgeId": item.knowledge_id,
        "snapshotId": item.snapshot_id,
        "versionNo": item.version_no,
        "status": item.status,
        "qualityStatus": item.quality_status,
        "changeSummary": _json_value(item.change_summary),
        "qualityReport": _json_value(item.quality_report),
        "processingRuleVersion": item.processing_rule_version,
        "generationModel": item.generation_model,
        "createdAt": item.created_at,
        "publishedAt": item.published_at,
    }


def _manifest_dict(item: KnowledgeManifest | None) -> dict | None:
    if item is None:
        return None
    return {
        "id": item.id,
        "versionId": item.version_id,
        "purpose": item.purpose,
        "applicableQuestions": item.applicable_questions,
        "inapplicableQuestions": item.inapplicable_questions,
        "coreTopics": item.core_topics,
        "entities": item.entities,
        "boundaries": item.boundaries,
        "changeLevel": item.change_level,
        "confirmed": item.confirmed == 1,
    }


def _job_dict(item: KnowledgeSyncJob) -> dict:
    return {
        "id": item.id,
        "knowledgeId": item.knowledge_id,
        "mode": item.mode,
        "status": item.status,
        "stage": item.stage,
        "progress": item.progress,
        "sourceError": item.source_error,
        "resultSummary": _json_value(item.result_summary),
        "createdAt": item.created_at,
        "startedAt": item.started_at,
        "finishedAt": item.finished_at,
    }


def _json_value(raw: str | None):
    if not raw:
        return None
    try:
        return json.loads(raw)
    except json.JSONDecodeError:
        return raw


def _escape_like(value: str) -> str:
    return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")


def _merge_content_search_rows(rows, keyword: str, chunk_section_ids: set[int]) -> list[dict]:
    grouped: dict[tuple[int, int, int], dict] = {}
    for row in rows:
        key = (int(row["knowledge_id"]), int(row["version_id"]), int(row["section_id"]))
        item_type = str(row["item_type"])
        current = grouped.get(key)
        if current is None:
            current = _content_search_item(row, keyword, int(row["section_id"]) in chunk_section_ids)
            current["itemTypes"] = []
            grouped[key] = current
        if item_type not in current["itemTypes"]:
            current["itemTypes"].append(item_type)
        for matched in _matched_in(str(row["title"] or ""), str(row["content"] or ""), keyword):
            if matched not in current["matchedIn"]:
                current["matchedIn"].append(matched)
        for term in _highlight_terms(str(row["title"] or ""), str(row["content"] or ""), keyword):
            if term not in current["highlightTerms"]:
                current["highlightTerms"].append(term)
        current["itemType"] = _primary_item_type(current["itemTypes"])
    return list(grouped.values())


def _content_search_item(row, keyword: str, has_chunks: bool) -> dict:
    title = str(row["title"] or "")
    content = str(row["content"] or "")
    is_active_current = (
        int(row["knowledge_status"] or 0) == 1
        and row["lifecycle_status"] == "active"
        and row["source_status"] == "normal"
        and bool(row["version_id"])
    )
    return {
        "knowledgeId": row["knowledge_id"],
        "knowledgeName": row["knowledge_name"],
        "sourceTitle": row["source_title"],
        "knowledgeType": row["knowledge_type"],
        "knowledgeStatus": row["knowledge_status"],
        "lifecycleStatus": row["lifecycle_status"],
        "sourceStatus": row["source_status"],
        "versionId": row["version_id"],
        "versionNo": row["version_no"],
        "publishedAt": row["published_at"],
        "itemType": row["item_type"],
        "itemId": row["item_id"],
        "sectionId": row["section_id"],
        "sectionKey": row["section_key"],
        "title": title,
        "snippet": _snippet(f"{title}\n{content}", keyword),
        "matchedIn": _matched_in(title, content, keyword),
        "highlightTerms": _highlight_terms(title, content, keyword),
        "itemTypes": [row["item_type"]],
        "hasChunks": has_chunks,
        "isCurrentVersion": True,
        "canAgentUse": is_active_current and has_chunks,
    }


def _primary_item_type(item_types: list[str]) -> str:
    for item_type in ("section", "card", "chunk"):
        if item_type in item_types:
            return item_type
    return item_types[0] if item_types else "section"


def _matched_in(title: str, content: str, keyword: str) -> list[str]:
    result: list[str] = []
    lowered = keyword.lower()
    if lowered in title.lower():
        result.append("title")
    if lowered in content.lower():
        result.append("content")
    return result or ["content"]


def _highlight_terms(title: str, content: str, keyword: str) -> list[str]:
    keyword = keyword.strip()
    if not keyword:
        return []
    text = f"{title}\n{content}".lower()
    terms = [keyword]
    if keyword.lower() not in text:
        terms.extend(_keyword_parts(keyword))
    result: list[str] = []
    for term in sorted({item.strip() for item in terms if item.strip()}, key=len, reverse=True):
        if term.lower() in text and term not in result:
            result.append(term)
    return result or [keyword]


def _keyword_parts(keyword: str) -> list[str]:
    parts = [part for part in keyword.replace("，", " ").replace(",", " ").split() if part]
    if parts:
        return parts
    return list(keyword) if len(keyword) <= 8 else []


def _snippet(text: str, keyword: str, radius: int = 90) -> str:
    compact = " ".join(text.split())
    if not compact:
        return ""
    index = compact.lower().find(keyword.lower())
    if index < 0:
        return compact[: radius * 2] + ("..." if len(compact) > radius * 2 else "")
    start = max(0, index - radius)
    end = min(len(compact), index + len(keyword) + radius)
    prefix = "..." if start > 0 else ""
    suffix = "..." if end < len(compact) else ""
    return f"{prefix}{compact[start:end]}{suffix}"
