"""文献 API：个性化 Feed、搜索、文献详情（需登录）"""

import uuid
from datetime import datetime, timezone

from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import PlainTextResponse
from pydantic import BaseModel
from sqlalchemy import case, func, or_, select, text
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.permissions import get_current_user
from app.db import get_db
from app.models.interaction import UserDismissedTag, UserFeed, UserLiterature
from app.models.literature import GlobalLiterature, GlobalLiteratureTag, GlobalTag
from app.schemas.literature import FeedResponse, LiteratureCard, LiteratureDetail, cap_pub_date
from app.core.cache import cache
from app.services.cos_client import cached_get_full_text
from app.services.tag_loader import load_tags_for_literature

router = APIRouter()
DISMISS_THRESHOLD = 3  # 同一标签被忽略 N 次后，Feed 引擎不再推送该标签下的文章


def uid(u: dict) -> uuid.UUID:
    return uuid.UUID(u["sub"])


async def _record_dismissed_tags(db: AsyncSession, user_id: uuid.UUID, feed_item: UserFeed):
    """提取 feed_item 的 matched_tags 并记录为负偏好信号（upsert 累计计数）"""
    if not feed_item.matched_tags:
        return
    for tag_info in (feed_item.matched_tags or []):
        tag_id_str = tag_info.get("tag_id")
        if not tag_id_str:
            continue
        try:
            tag_uuid = uuid.UUID(tag_id_str)
        except (ValueError, TypeError):
            continue
        existing = await db.execute(
            select(UserDismissedTag).where(
                UserDismissedTag.user_id == user_id,
                UserDismissedTag.tag_id == tag_uuid,
            )
        )
        dt = existing.scalar()
        if dt:
            dt.dismiss_count += 1
            dt.last_dismissed_at = datetime.now(timezone.utc)
        else:
            db.add(UserDismissedTag(
                user_id=user_id, tag_id=tag_uuid, dismiss_count=1,
            ))


@router.get("/feed", response_model=FeedResponse, summary="个人Feed")
async def personal_feed(
    db: AsyncSession = Depends(get_db),
    user: dict = Depends(get_current_user),
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=100),
    priority: str | None = Query(None, description="筛选优先级"),
    date_from: str | None = Query(None, description="推送日期起始 (YYYY-MM-DD)"),
    date_to: str | None = Query(None, description="推送日期截止 (YYYY-MM-DD)"),
    q: str = Query("", description="搜索标题/摘要"),
):
    user_id = uid(user)
    offset = (page - 1) * page_size

    # ── 构建筛选条件 ──
    filters = [UserFeed.user_id == user_id, UserFeed.is_dismissed == False]
    if priority:
        filters.append(UserFeed.priority == priority)
    if date_from:
        filters.append(UserFeed.pushed_at >= date_from)
    if date_to:
        filters.append(UserFeed.pushed_at <= date_to + " 23:59:59")

    # content search 需要 JOIN literature
    if q.strip():
        like = f"%{q.strip()}%"
        content_filter = select(UserFeed.id).join(
            GlobalLiterature, UserFeed.literature_id == GlobalLiterature.id
        ).where(
            GlobalLiterature.title.ilike(like) | GlobalLiterature.abstract.ilike(like)
        )
        filters.append(UserFeed.id.in_(content_filter))

    count_q = select(func.count(UserFeed.id)).where(*filters)
    total = (await db.execute(count_q)).scalar() or 0

    q = (
        select(UserFeed, GlobalLiterature)
        .join(GlobalLiterature, UserFeed.literature_id == GlobalLiterature.id)
        .where(*filters)
        .order_by(text("CASE WHEN user_feed.priority='must_read' THEN 0 WHEN user_feed.priority='recommended' THEN 1 ELSE 2 END"))
        .offset(offset).limit(page_size)
    )
    result = await db.execute(q)

    # 收集原始数据
    raw = []
    lit_ids = []
    for feed_item, lit in result:
        lit_ids.append(str(lit.id))
        raw.append((feed_item, lit))

    tm = await load_tags_for_literature(db, lit_ids)

    items = []
    for feed_item, lit in raw:
        authors = lit.authors or []
        affiliation = None
        for a in authors:
            aff = a.get("affiliation","") or ""
            if aff.strip(): affiliation = aff.split(",")[0].strip()[:30]; break
        tlist = tm.get(str(lit.id), [])
        for t in tlist:
            if '::' in t.get('path', ''):
                t['name_zh'] = t['path'].split('::')[-1]
        items.append(LiteratureCard(
            id=str(lit.id), pmid=lit.pmid, title=lit.title,
            first_author=authors[0].get("family", "") if authors else "",
            affiliation=affiliation, journal=lit.journal,
            pub_date=cap_pub_date(lit.pub_date),
            article_date=lit.article_date.isoformat() if lit.article_date else None,
            doi=lit.doi, pmc_id=lit.pmc_id, is_oa=lit.is_oa,
            cited_by_count=lit.cited_by_count, pub_types=lit.pub_types or [],
            tags=tlist, priority=feed_item.priority,
            is_read=feed_item.is_read,
            study_design=lit.study_design,
            trial_reg=lit.trial_reg,
            retracted=lit.retracted,
            is_negative_result=lit.is_negative_result,
            created_at=lit.created_at.isoformat() if lit.created_at else None,
            updated_at=lit.updated_at.isoformat() if lit.updated_at else None,
        ))

    priority_counts = await db.execute(
        select(UserFeed.priority, func.count(UserFeed.id))
        .where(
            UserFeed.user_id == user_id,
            UserFeed.is_dismissed == False,
            UserFeed.priority.in_(["must_read", "recommended", "related"]),
        )
        .group_by(UserFeed.priority)
    )
    counts = dict(priority_counts.all())
    must_read = counts.get("must_read", 0)
    recommended = counts.get("recommended", 0)
    related = counts.get("related", 0)
    return FeedResponse(
        items=items, total=total, date="today",
        must_read=must_read, recommended=recommended, related=related,
    )


class BatchFeedPmids(BaseModel):
    pmids: list[int]

class SaveBody(BaseModel):
    folder_id: str | None = None

class FolderMoveBody(BaseModel):
    folder_id: str | None = None


@router.post("/feed/batch-save", summary="批量收藏")
async def batch_save_feed(req: BatchFeedPmids, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
    """批量收藏 Feed 中的文献。已收藏的自动跳过。"""
    user_id = uid(user)
    saved = 0
    for pmid in req.pmids[:200]:
        lit = (await db.execute(select(GlobalLiterature.id).where(GlobalLiterature.pmid == pmid))).scalar()
        if not lit:
            continue
        exists = (await db.execute(
            select(UserLiterature).where(UserLiterature.user_id == user_id, UserLiterature.literature_id == lit)
        )).scalar()
        if not exists:
            db.add(UserLiterature(user_id=user_id, literature_id=lit))
            saved += 1
    await db.commit()
    return {"saved": saved, "total": len(req.pmids)}


@router.post("/feed/batch-dismiss", summary="批量忽略")
async def batch_dismiss_feed(req: BatchFeedPmids, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
    """批量忽略 Feed 中的文献推荐。"""
    user_id = uid(user)
    dismissed = 0
    for pmid in req.pmids[:200]:
        lit = (await db.execute(select(GlobalLiterature.id).where(GlobalLiterature.pmid == pmid))).scalar()
        if not lit:
            continue
        feed_item = (await db.execute(
            select(UserFeed).where(UserFeed.user_id == user_id, UserFeed.literature_id == lit)
        )).scalar()
        if feed_item and not feed_item.is_dismissed:
            feed_item.is_dismissed = True
            dismissed += 1
            await _record_dismissed_tags(db, user_id, feed_item)
    await db.commit()
    return {"dismissed": dismissed, "total": len(req.pmids)}


@router.post("/feed/{pmid}/read", summary="标记已读")
async def mark_feed_read(pmid: int, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
    """标记 Feed 中的某篇文献为已读"""
    lit = (await db.execute(select(GlobalLiterature.id).where(GlobalLiterature.pmid == pmid))).scalar()
    if not lit:
        raise HTTPException(status_code=404, detail="Not found")
    feed_item = (await db.execute(
        select(UserFeed).where(UserFeed.user_id == uid(user), UserFeed.literature_id == lit)
    )).scalar()
    if feed_item and not feed_item.is_read:
        feed_item.is_read = True
        feed_item.read_at = func.now()
        await db.commit()
    return {"status": "read"}


# ─── 静态路由必须在动态路由之前 ───

@router.get("/search", response_model=dict, summary="搜索文献")
async def search_literature(
    q: str = Query(""),
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=100),
    db: AsyncSession = Depends(get_db),
    user: dict = Depends(get_current_user),
):
    if not q.strip():
        return {"items": [], "total": 0}
    offset = (page - 1) * page_size
    like = f"%{q}%"
    # tsvector 主搜索 + ILIKE 兜底
    search_cond = or_(
        GlobalLiterature.search_tsv.op("@@")(func.plainto_tsquery("english", q)),
        GlobalLiterature.title.ilike(like),
        GlobalLiterature.abstract.ilike(like),
    )
    count_q = select(func.count(GlobalLiterature.id)).where(search_cond)
    total = (await db.execute(count_q)).scalar() or 0
    tsq = func.plainto_tsquery("english", q)
    best_match_rank = (
        func.ts_rank(GlobalLiterature.search_tsv, tsq) * 0.3
        + func.ln(func.coalesce(GlobalLiterature.cited_by_count, 0) + 1) * 2
        + case((GlobalLiterature.pub_year >= 2020, 5), else_=0)
    )
    result = await db.execute(
        select(GlobalLiterature).where(search_cond)
        .order_by(best_match_rank.desc()).offset(offset).limit(page_size)
    )
    lit_list = result.scalars().all()
    tm = await load_tags_for_literature(db, [str(lit.id) for lit in lit_list])
    items = []
    for lit in lit_list:
        authors = lit.authors or []
        affiliation = None
        for a in authors:
            aff = a.get("affiliation","") or ""
            if aff.strip(): affiliation = aff.split(",")[0].strip()[:30]; break
        items.append(LiteratureCard(
            id=str(lit.id), pmid=lit.pmid, title=lit.title,
            first_author=authors[0].get("family", "") if authors else "",
            affiliation=affiliation, journal=lit.journal,
            pub_date=cap_pub_date(lit.pub_date),
            article_date=lit.article_date.isoformat() if lit.article_date else None,
            doi=lit.doi, pmc_id=lit.pmc_id, is_oa=lit.is_oa,
            cited_by_count=lit.cited_by_count, pub_types=lit.pub_types or [],
            tags=tm.get(str(lit.id), []),
            study_design=lit.study_design,
            trial_reg=lit.trial_reg,
            retracted=lit.retracted,
            is_negative_result=lit.is_negative_result,
            created_at=lit.created_at.isoformat() if lit.created_at else None,
            updated_at=lit.updated_at.isoformat() if lit.updated_at else None,
        ))
    return {"items": items, "total": total}


@router.get("/saved", response_model=dict, summary="收藏列表")
async def saved_list(
    db: AsyncSession = Depends(get_db),
    user: dict = Depends(get_current_user),
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=100),
    folder_id: str | None = Query(None, description="按文件夹筛选"),
):
    offset = (page - 1) * page_size
    user_uuid = uid(user)
    conditions = [UserLiterature.user_id == user_uuid]
    if folder_id:
        try:
            conditions.append(UserLiterature.folder_id == uuid.UUID(folder_id))
        except ValueError:
            pass
    total = (await db.execute(
        select(func.count(UserLiterature.id)).where(*conditions)
    )).scalar() or 0
    result = await db.execute(
        select(UserLiterature, GlobalLiterature)
        .join(GlobalLiterature, UserLiterature.literature_id == GlobalLiterature.id)
        .where(*conditions)
        .order_by(UserLiterature.saved_at.desc()).offset(offset).limit(page_size)
    )
    raw = [(ul, lit) for ul, lit in result]
    lit_ids = [str(lit.id) for _, lit in raw]
    tm = await load_tags_for_literature(db, lit_ids)

    items = []
    for _ul, lit in raw:
        authors = lit.authors or []
        affiliation = None
        for a in authors:
            aff = a.get("affiliation","") or ""
            if aff.strip(): affiliation = aff.split(",")[0].strip()[:30]; break
        items.append(LiteratureCard(
            id=str(lit.id), pmid=lit.pmid, title=lit.title,
            first_author=authors[0].get("family", "") if authors else "",
            affiliation=affiliation, journal=lit.journal,
            pub_date=cap_pub_date(lit.pub_date),
            article_date=lit.article_date.isoformat() if lit.article_date else None,
            doi=lit.doi, pmc_id=lit.pmc_id, is_oa=lit.is_oa,
            cited_by_count=lit.cited_by_count, pub_types=lit.pub_types or [],
            tags=tm.get(str(lit.id), []),
            study_design=lit.study_design,
            trial_reg=lit.trial_reg,
            retracted=lit.retracted,
            is_negative_result=lit.is_negative_result,
            created_at=lit.created_at.isoformat() if lit.created_at else None,
            updated_at=lit.updated_at.isoformat() if lit.updated_at else None,
            rating=_ul.rating,
            personal_tags=_ul.personal_tags or [],
            reading_status=_ul.reading_status,
        ))
    return {"items": items, "total": total}


# ─── 批量操作（静态路由必须在 /{pmid} 之前）───


class BulkAction(BaseModel):
    pmids: list[int]


@router.post("/bulk/save", response_model=dict, summary="批量收藏")
async def bulk_save(req: BulkAction, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
    """批量收藏"""
    pmids = req.pmids[:100]
    if not pmids: return {"status": "saved", "count": 0}
    user_uuid = uid(user)
    # 批量预查文献 ID
    lit_result = await db.execute(
        select(GlobalLiterature.id, GlobalLiterature.pmid).where(GlobalLiterature.pmid.in_(pmids))
    )
    pmid_to_lit = {pmid: lid for lid, pmid in lit_result.all()}
    if not pmid_to_lit: return {"status": "saved", "count": 0}
    # 批量过滤已收藏
    existing = await db.execute(
        select(UserLiterature.literature_id).where(
            UserLiterature.user_id == user_uuid,
            UserLiterature.literature_id.in_(list(pmid_to_lit.values()))
        )
    )
    saved_ids = set(existing.scalars().all())
    # 批量添加新收藏
    new_entries = [
        UserLiterature(user_id=user_uuid, literature_id=lit_id)
        for lit_id in pmid_to_lit.values() if lit_id not in saved_ids
    ]
    db.add_all(new_entries)
    await db.commit()
    return {"status": "saved", "count": len(new_entries)}


# ─── 动态路由 ───

@router.get("/{pmid}", response_model=LiteratureDetail, summary="文献详情")
async def literature_detail(pmid: int, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
    result = await db.execute(select(GlobalLiterature).where(GlobalLiterature.pmid == pmid))
    lit = result.scalar()
    if not lit:
        raise HTTPException(status_code=404, detail="Not found")

    tags = []
    tag_result = await db.execute(
        select(GlobalTag, GlobalLiteratureTag.is_major)
        .join(GlobalTag, GlobalTag.id == GlobalLiteratureTag.tag_id)
        .where(
            GlobalLiteratureTag.literature_id == lit.id,
            GlobalTag.is_active == True,
        )
    )
    for tag, is_major in tag_result:
        tags.append({"id": str(tag.id), "name_zh": tag.name_zh, "name_en": tag.name_en,
                      "path": tag.path, "category": tag.tag_category, "is_major": is_major})

    authors = lit.authors or []

    # Auto-mark feed item as read
    feed_item = await db.execute(
        select(UserFeed).where(
            UserFeed.user_id == uid(user),
            UserFeed.literature_id == lit.id,
            UserFeed.is_read == False,
        )
    )
    feed = feed_item.scalar()
    if feed:
        feed.is_read = True
        feed.read_at = func.now()

    # Auto-mark saved literature reading_status as "reading" if unread
    reading_status: str | None = None
    ul = (await db.execute(
        select(UserLiterature).where(
            UserLiterature.user_id == uid(user),
            UserLiterature.literature_id == lit.id,
        )
    )).scalar()
    if ul:
        reading_status = ul.reading_status
        if reading_status == "unread":
            ul.reading_status = "reading"
            ul.read_started_at = func.now()
            reading_status = "reading"
    await db.commit()

    return LiteratureDetail(
        id=str(lit.id), pmid=lit.pmid, title=lit.title,
        abstract=lit.abstract, authors=authors,
        affiliation=authors[0].get("affiliation", "") if authors else "",
        doi=lit.doi, pmc_id=lit.pmc_id, is_oa=lit.is_oa,
        cited_by_count=lit.cited_by_count,
        journal=lit.journal,
        pub_date=cap_pub_date(lit.pub_date),
        pub_year=lit.pub_year, pub_types=lit.pub_types or [],
        grants=lit.grants or [], keywords=lit.keywords or [],
        tags=tags, language=lit.language or "en",
        study_design=lit.study_design,
        trial_reg=lit.trial_reg,
        pico=lit.pico,
        retracted=lit.retracted,
        retraction_details=lit.retraction_details,
        is_negative_result=lit.is_negative_result,
        negative_result_details=lit.negative_result_details,
        reading_status=reading_status,
        license_info=lit.license_info,
        pre_extracted_data=lit.pre_extracted_data,
        full_text_sections=await cached_get_full_text(lit.pmid, lit),
        chemical_list=lit.chemical_list or [],
        gene_symbols=lit.gene_symbols or [],
        num_refs=lit.num_refs,
        publication_status=lit.publication_status,
        article_date=lit.article_date.isoformat() if lit.article_date else None,
        databank_list=lit.databank_list or [],
        suppl_mesh_list=lit.suppl_mesh_list or [],
        created_at=lit.created_at.isoformat() if lit.created_at else None,
        updated_at=lit.updated_at.isoformat() if lit.updated_at else None,
    )


@router.post("/{pmid}/save", summary="收藏文献")
async def save_literature(pmid: int, body: SaveBody | None = None, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
    result = await db.execute(select(GlobalLiterature).where(GlobalLiterature.pmid == pmid))
    lit = result.scalar()
    if not lit:
        raise HTTPException(status_code=404, detail="Not found")
    existing = await db.execute(
        select(UserLiterature).where(UserLiterature.user_id == uid(user), UserLiterature.literature_id == lit.id)
    )
    existing_ul = existing.scalar()
    if existing_ul:
        if body and body.folder_id:
            existing_ul.folder_id = uuid.UUID(body.folder_id)
            await db.commit()
        return {"status": "already_saved"}
    ul = UserLiterature(user_id=uid(user), literature_id=lit.id)
    if body and body.folder_id:
        ul.folder_id = uuid.UUID(body.folder_id)
    db.add(ul)
    await db.commit()
    return {"status": "saved"}


@router.put("/{pmid}/folder", summary="移动文献到文件夹（null=移出）")
async def move_to_folder(pmid: int, body: FolderMoveBody, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
    result = await db.execute(select(GlobalLiterature).where(GlobalLiterature.pmid == pmid))
    lit = result.scalar()
    if not lit:
        raise HTTPException(status_code=404, detail="Not found")
    ul = (await db.execute(
        select(UserLiterature).where(UserLiterature.user_id == uid(user), UserLiterature.literature_id == lit.id)
    )).scalar()
    if not ul:
        raise HTTPException(400, detail="Save first, then move")
    ul.folder_id = uuid.UUID(body.folder_id) if body.folder_id else None
    await db.commit()
    return {"status": "moved"}


@router.delete("/{pmid}/save", summary="取消收藏")
async def unsave_literature(pmid: int, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
    result = await db.execute(select(GlobalLiterature).where(GlobalLiterature.pmid == pmid))
    lit = result.scalar()
    if not lit:
        raise HTTPException(status_code=404, detail="Not found")
    existing = await db.execute(
        select(UserLiterature).where(UserLiterature.user_id == uid(user), UserLiterature.literature_id == lit.id)
    )
    ul = existing.scalar()
    if not ul:
        return {"status": "not_saved"}
    await db.delete(ul)
    await db.commit()
    return {"status": "unsaved"}


class ReadingStatusBody(BaseModel):
    status: str  # unread / reading / completed


@router.put("/{pmid}/reading-status", summary="更新阅读状态")
async def update_reading_status(pmid: int, body: ReadingStatusBody, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
    """更新收藏文献的阅读状态：unread / reading / completed"""
    if body.status not in ("unread", "reading", "completed"):
        raise HTTPException(400, detail="Invalid status")
    result = await db.execute(select(GlobalLiterature).where(GlobalLiterature.pmid == pmid))
    lit = result.scalar()
    if not lit:
        raise HTTPException(status_code=404, detail="Not found")
    ul = (await db.execute(
        select(UserLiterature).where(UserLiterature.user_id == uid(user), UserLiterature.literature_id == lit.id)
    )).scalar()
    if not ul:
        raise HTTPException(400, detail="Save first, then set reading status")
    ul.reading_status = body.status
    if body.status == "reading" and not ul.read_started_at:
        ul.read_started_at = func.now()
    if body.status == "completed":
        ul.read_finished_at = func.now()
    await db.commit()
    return {"status": body.status}


@router.post("/{pmid}/dismiss", summary="忽略推荐")
async def dismiss_feed(pmid: int, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
    result = await db.execute(select(GlobalLiterature).where(GlobalLiterature.pmid == pmid))
    lit = result.scalar()
    if not lit:
        raise HTTPException(status_code=404, detail="Not found")
    feed_result = await db.execute(
        select(UserFeed).where(UserFeed.user_id == uid(user), UserFeed.literature_id == lit.id)
    )
    feed_item = feed_result.scalar()
    if feed_item:
        feed_item.is_dismissed = True
        await _record_dismissed_tags(db, uid(user), feed_item)
        await db.commit()
    return {"status": "dismissed"}


# ─── 导出（静态路由必须在 /{pmid} 之前）───


@router.get("/export/{pmid}/{fmt}", response_class=PlainTextResponse, summary="单篇导出")
async def export_single_endpoint(pmid: int, fmt: str, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
    """单篇导出 BibTeX/RIS/EndNote/CSV/MLA"""
    if fmt not in ("bibtex", "ris", "endnote", "csv", "mla"):
        raise HTTPException(status_code=400, detail="Unsupported format")
    result = await db.execute(select(GlobalLiterature).where(GlobalLiterature.pmid == pmid))
    lit = result.scalar()
    if not lit: raise HTTPException(404)
    from app.services.citation_export import export_single
    lit_dict = {"pmid": lit.pmid, "title": lit.title, "authors": lit.authors or [],
                "journal": lit.journal, "pub_year": lit.pub_year, "volume": lit.volume,
                "issue": lit.issue, "pages": lit.pages, "doi": lit.doi}
    content = export_single(lit_dict, fmt)
    ext_map = {"bibtex": "bib", "ris": "ris", "endnote": "enw", "csv": "csv", "mla": "mla"}
    return PlainTextResponse(content, media_type="text/plain",
                             headers={"Content-Disposition": f"attachment; filename=pubmed_{pmid}.{ext_map[fmt]}"})


@router.post("/export/batch/{fmt}", response_class=PlainTextResponse, summary="批量导出")
async def export_batch_endpoint(fmt: str, pmids: list[int] = Query(default=[]), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
    """批量导出"""
    if fmt not in ("bibtex", "ris", "endnote", "csv", "mla"):
        raise HTTPException(status_code=400, detail="Unsupported format")
    if not pmids:
        raise HTTPException(status_code=400, detail="No PMIDs provided")
    result = await db.execute(select(GlobalLiterature).where(GlobalLiterature.pmid.in_(pmids[:200])))
    articles = result.scalars().all()
    from app.services.citation_export import export_batch
    lit_dicts = [{"pmid": a.pmid, "title": a.title, "authors": a.authors or [],
                  "journal": a.journal, "pub_year": a.pub_year, "volume": a.volume,
                  "issue": a.issue, "pages": a.pages, "doi": a.doi} for a in articles]
    content = export_batch(lit_dicts, fmt)
    return PlainTextResponse(content, media_type="text/plain",
                             headers={"Content-Disposition": f"attachment; filename=export.{fmt}"})


@router.get("/export/{pmid}/citation", summary="获取引用文本（剪贴板）")
async def export_citation_endpoint(pmid: int, fmt: str = Query("mla"), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
    """返回 JSON 格式的引用文本，适合前端 clipboard API"""
    if fmt not in ("bibtex", "ris", "endnote", "csv", "mla"):
        raise HTTPException(status_code=400, detail="Unsupported format")
    result = await db.execute(select(GlobalLiterature).where(GlobalLiterature.pmid == pmid))
    lit = result.scalar()
    if not lit:
        raise HTTPException(404)
    from app.services.citation_export import export_single
    lit_dict = {"pmid": lit.pmid, "title": lit.title, "authors": lit.authors or [],
                "journal": lit.journal, "pub_year": lit.pub_year, "volume": lit.volume,
                "issue": lit.issue, "pages": lit.pages, "doi": lit.doi}
    content = export_single(lit_dict, fmt)
    return {"citation": content, "format": fmt}


async def _get_related_literature(
    db: AsyncSession, lit: GlobalLiterature, limit: int = 10,
) -> tuple[list, str]:
    """Core logic: find literature related to `lit` by tag overlap + citations + journal + co-author.
    Returns (items, source) where source is 'tags'|'journal'|'none'.

    Performance: per-tag LIMIT 200 + Python Counter avoids seq scan on
    global_literature_tags (1.18M rows) that ORM GROUP BY + tag_id IN causes.
    """
    from collections import Counter
    from datetime import datetime
    from app.compat import UTC

    tag_result = await db.execute(
        select(GlobalLiteratureTag.tag_id)
        .where(GlobalLiteratureTag.literature_id == lit.id)
    )
    src_tag_ids = {row[0] for row in tag_result}

    if not src_tag_ids:
        fallback_rows = (await db.execute(
            select(GlobalLiterature)
            .where(
                GlobalLiterature.journal == lit.journal,
                GlobalLiterature.id != lit.id,
                GlobalLiterature.pub_date.isnot(None),
            )
            .order_by(GlobalLiterature.cited_by_count.desc().nullslast())
            .limit(limit)
        )).scalars().all()
        return _build_related_cards(fallback_rows), "journal"

    # 每标签取 top-200 候选（ix_glt_tag 索引扫描，~0.2ms/标签）
    # 并行执行所有标签查询
    import asyncio as _asyncio

    async def _fetch_for_tag(tag_id) -> list:
        rows = await db.execute(
            select(GlobalLiteratureTag.literature_id)
            .where(GlobalLiteratureTag.tag_id == tag_id)
            .limit(200)
        )
        return [r[0] for r in rows if r[0] != lit.id]

    tag_results = await _asyncio.gather(
        *[_fetch_for_tag(tid) for tid in src_tag_ids],
    )

    # Python Counter: 统计所有候选文献的标签重叠数
    all_candidates = Counter()
    for lit_ids in tag_results:
        for lid in lit_ids:
            all_candidates[lid] += 1

    # 取重叠数最高的候选
    top_ids = [lid for lid, _ in all_candidates.most_common(30)]
    if not top_ids:
        return [], "none"

    candidates = (await db.execute(
        select(GlobalLiterature).where(GlobalLiterature.id.in_(top_ids))
    )).scalars().all()

    overlap_map = dict(all_candidates)

    scored = []
    for c in candidates:
        score = overlap_map.get(c.id, 0) * 10
        cited = c.cited_by_count or 0
        if cited > 50:
            score += 5
        elif cited > 10:
            score += 3
        elif cited > 0:
            score += 1

        if c.journal and lit.journal and c.journal.lower() == lit.journal.lower():
            score += 3

        if c.pub_date:
            days_old = (datetime.now(UTC).date() - c.pub_date).days
            if days_old < 90:
                score += 5
            elif days_old < 365:
                score += 2

        if c.authors and lit.authors:
            src_families = {a.get("family", "") for a in lit.authors if a.get("family")}
            cand_families = {a.get("family", "") for a in c.authors if a.get("family")}
            if src_families & cand_families:
                score += 8

        authors = c.authors or []
        scored.append({
            "pmid": c.pmid,
            "title": c.title,
            "journal": c.journal,
            "pub_date": cap_pub_date(c.pub_date),
            "article_date": c.article_date.isoformat() if c.article_date else None,
            "cited_by_count": cited,
            "first_author": authors[0].get("family", "") if authors else "",
            "overlap_tags": score // 10,
            "relevance_score": score,
        })

    scored.sort(key=lambda x: x["relevance_score"], reverse=True)
    return scored[:limit], "tags"


@router.get("/{pmid}/related", summary="相关文献推荐")
async def related_literature(pmid: int, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
    """根据标签重叠 + 同期刊 + 引用关系推荐相关文献"""
    key = f"related:{pmid}"
    cached = await cache.get(key)
    if cached:
        return cached
    lit = (await db.execute(select(GlobalLiterature).where(GlobalLiterature.pmid == pmid))).scalar()
    if not lit:
        raise HTTPException(status_code=404, detail="Not found")
    items, source = await _get_related_literature(db, lit, limit=10)
    result = {"items": items, "source": source}
    await cache.set(key, result, ttl=600)
    return result


def _build_related_cards(rows: list) -> list:
    """Build related literature card dicts from ORM rows."""
    items = []
    for c in rows:
        authors = c.authors or []
        items.append({
            "pmid": c.pmid,
            "title": c.title,
            "journal": c.journal,
            "pub_date": cap_pub_date(c.pub_date),
            "cited_by_count": c.cited_by_count or 0,
            "first_author": authors[0].get("family", "") if authors else "",
            "relevance_score": 0,
        })
    return items
