"""平台管理 API：仅超级用户可访问"""

import uuid
from datetime import datetime

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

from app.compat import UTC
from app.core.cache import cache
from app.core.permissions import get_current_user, require_platform_admin, require_platform_operator, require_platform_viewer
from app.db import async_session, get_db
from app.models.literature import GlobalTag, GlobalLiteratureTag, GlobalTagTreeNumber
from app.services.cos_client import get_full_text, invalidate_full_text_cache, upload_json
from app.services.geoip import lookup_ip


class TenantUpdate(BaseModel):
    name: str | None = None
    hospital_name: str | None = None
    plan_type: str | None = None
    status: str | None = None
    max_members: int | None = None


class UserUpdate(BaseModel):
    is_active: bool | None = None
    title: str | None = None
    admin_note: str | None = None
    customer_name: str | None = None


class NotificationCreate(BaseModel):
    title: str
    content: str = ""
    target_type: str = "all"
    target_id: str | None = None
from app.models.business import AiProviderConfig, ApprovalStep, ApprovalWorkflow, DrugApproval, GuidelineEvidence, GuidelineVersion
from app.models.interaction import AnonymousPageView, UserActivityLog, UserJournalSubscription, UserLiterature, UserNote
from app.models.literature import GlobalJournal, GlobalLiterature, GlobalLiteratureTag, GlobalTag
from app.models.operations import PipelineRun, SystemNotification, TenantSubscription
from app.models.user import LoginLog, Tenant, User, UserTenant


# 所有管理端路由强制要求平台角色（最低 VIEWER，敏感操作额外限制）
router = APIRouter(dependencies=[Depends(require_platform_viewer)])

# ─── 租户管理 ───

@router.get("/tenants", response_model=dict, summary="租户列表")
async def list_tenants(
    db: AsyncSession = Depends(get_db),
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=100),
    q: str = Query(""),
):
    qs = select(Tenant)
    if q:
        qs = qs.where(Tenant.name.ilike(f"%{q}%"))
    total = (await db.execute(select(func.count()).select_from(qs.subquery()))).scalar() or 0
    result = await db.execute(qs.order_by(Tenant.created_at.desc()).offset((page-1)*page_size).limit(page_size))
    tenants = result.scalars().all()
    return {
        "items": [{"id": str(t.id), "name": t.name, "slug": t.slug, "hospital_name": t.hospital_name,
                    "plan_type": t.plan_type, "is_personal": t.is_personal, "status": t.status,
                    "created_at": t.created_at.isoformat() if t.created_at else None} for t in tenants],
        "total": total,
    }

@router.get("/tenants/{tid}", response_model=dict, summary="租户详情")
async def get_tenant(tid: str, db: AsyncSession = Depends(get_db)):
    t = (await db.execute(select(Tenant).where(Tenant.id == uuid.UUID(tid)))).scalar()
    if not t: raise HTTPException(404, "Tenant not found")
    member_count = (await db.execute(select(func.count(UserTenant.id)).where(UserTenant.tenant_id == uuid.UUID(tid)))).scalar() or 0
    sub = (await db.execute(select(TenantSubscription).where(TenantSubscription.tenant_id == uuid.UUID(tid)))).scalar()
    return {"id": str(t.id), "name": t.name, "slug": t.slug, "hospital_name": t.hospital_name,
            "plan_type": t.plan_type, "is_personal": t.is_personal, "status": t.status,
            "max_members": t.max_members, "settings": t.settings, "member_count": member_count,
            "subscription": {"status": sub.status, "plan_id": sub.plan_id, "current_period_end": sub.current_period_end.isoformat() if sub and sub.current_period_end else None} if sub else None,
            "created_at": t.created_at.isoformat() if t.created_at else None}

@router.put("/tenants/{tid}", response_model=dict, summary="更新租户")
async def update_tenant(tid: str, data: TenantUpdate, db: AsyncSession = Depends(get_db),
                        _admin: dict = Depends(require_platform_admin)):
    t = (await db.execute(select(Tenant).where(Tenant.id == uuid.UUID(tid)))).scalar()
    if not t: raise HTTPException(404)
    update_data = data.model_dump(exclude_none=True)
    for k, v in update_data.items():
        setattr(t, k, v)
    await db.commit()
    return {"status": "updated"}

@router.post("/tenants/{tid}/toggle-status", response_model=dict, summary="启停租户")
async def toggle_tenant_status(tid: str, db: AsyncSession = Depends(get_db),
                               _admin: dict = Depends(require_platform_admin)):
    t = (await db.execute(select(Tenant).where(Tenant.id == uuid.UUID(tid)))).scalar()
    if not t: raise HTTPException(404)
    t.status = "active" if t.status != "active" else "suspended"
    await db.commit()
    return {"status": t.status, "new_status": t.status}

# ─── 用户管理 ───

@router.get("/users", response_model=dict, summary="用户列表")
async def list_users(
    db: AsyncSession = Depends(get_db),
    page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100),
    q: str = Query(""),
    tenant_id: str = Query(""),
):
    qs = select(User)
    if q: qs = qs.where(User.email.ilike(f"%{q}%") | User.display_name.ilike(f"%{q}%") | User.customer_name.ilike(f"%{q}%") | User.admin_note.ilike(f"%{q}%"))
    # 按租户筛选
    if tenant_id:
        try:
            tid_uuid = uuid.UUID(tenant_id)
            subq = select(UserTenant.user_id).where(UserTenant.tenant_id == tid_uuid)
            qs = qs.where(User.id.in_(subq))
        except ValueError:
            pass
    total = (await db.execute(select(func.count()).select_from(qs.subquery()))).scalar() or 0
    result = await db.execute(qs.order_by(User.created_at.desc()).offset((page-1)*page_size).limit(page_size))
    users = result.scalars().all()
    # 批量加载所有用户的租户关联（N+1 → 1）
    user_ids = [u.id for u in users]
    ut_rows = (await db.execute(
        select(UserTenant.user_id, UserTenant.role, Tenant.name, Tenant.id)
        .join(Tenant, UserTenant.tenant_id == Tenant.id)
        .where(UserTenant.user_id.in_(user_ids))
    )).all()
    ut_map: dict[uuid.UUID, list[dict]] = {}
    for uid_val, role, t_name, t_id in ut_rows:
        ut_map.setdefault(uid_val, []).append({
            "tenant_name": t_name, "tenant_id": str(t_id), "role": role,
        })

    items = []
    for u in users:
        tenant_roles = ut_map.get(u.id, [])
        items.append({"id": str(u.id), "email": u.email, "display_name": u.display_name, "title": u.title,
                       "is_active": u.is_active, "platform_role": u.platform_role,
                       "tenant_count": len(tenant_roles), "tenants": tenant_roles,
                       "admin_note": u.admin_note, "customer_name": u.customer_name,
                       "last_login_at": u.last_login_at.isoformat() if u.last_login_at else None,
                       "created_at": u.created_at.isoformat() if u.created_at else None})
    return {"items": items, "total": total}

@router.put("/users/{uid}", response_model=dict, summary="更新用户")
async def update_user(uid: str, data: UserUpdate, db: AsyncSession = Depends(get_db),
                      _admin: dict = Depends(require_platform_admin)):
    try:
        uid_uuid = uuid.UUID(uid)
    except ValueError:
        raise HTTPException(400, "Invalid user ID")
    u = (await db.execute(select(User).where(User.id == uid_uuid))).scalar()
    if not u: raise HTTPException(404)
    update_data = data.model_dump(exclude_none=True)
    for k, v in update_data.items():
        setattr(u, k, v)
    await db.commit()
    return {"status": "updated"}

# ─── 看板数据 ───

@router.get("/dashboard", response_model=dict, summary="控制面板")
async def dashboard_stats():
    cached = await cache.get("admin:dashboard")
    if cached:
        return cached
    from datetime import datetime, timedelta
    week_ago = datetime.now(UTC) - timedelta(days=7)
    async with async_session() as db:
        tc = (await db.execute(select(func.count(Tenant.id)))).scalar() or 0
        uc = (await db.execute(select(func.count(User.id)))).scalar() or 0
        lc = (await db.execute(select(func.count(GlobalLiterature.id)))).scalar() or 0
        nc = (await db.execute(select(func.count(UserNote.id)))).scalar() or 0
        sc = (await db.execute(select(func.count(UserLiterature.id)))).scalar() or 0
        wau = (await db.execute(select(func.count(func.distinct(LoginLog.user_id))).where(LoginLog.created_at >= week_ago))).scalar() or 0
        recent_pipelines = (await db.execute(select(PipelineRun).order_by(PipelineRun.created_at.desc()).limit(5))).scalars().all()
        r = await db.execute(select(func.count(GlobalLiterature.id)).where(GlobalLiterature.abstract.isnot(None), GlobalLiterature.abstract != ""))
        abstract_ok = r.scalar() or 0
        r = await db.execute(select(func.count(GlobalLiterature.id)).where(
            (GlobalLiterature.full_text_sections.isnot(None)) | (GlobalLiterature.full_text_path.isnot(None))
        ))
        fulltext_ok = r.scalar() or 0
        r = await db.execute(select(func.count(func.distinct(GlobalLiteratureTag.literature_id))))
        tagged = r.scalar() or 0
        r = await db.execute(select(func.count(GlobalLiterature.id)).where(GlobalLiterature.pico.isnot(None)))
        pico_ok = r.scalar() or 0
        r = await db.execute(select(func.count(GlobalLiterature.id)).where(GlobalLiterature.study_design.isnot(None)))
        design_ok = r.scalar() or 0
        r = await db.execute(select(func.count(GlobalLiterature.id)).where(GlobalLiterature.cited_by_count.isnot(None)))
        cited_ok = r.scalar() or 0
        r = await db.execute(select(func.count(GlobalLiterature.id)).where(GlobalLiterature.doi.isnot(None)))
        doi_ok = r.scalar() or 0
        r = await db.execute(
            select(PipelineRun.run_type, func.count(PipelineRun.id), func.sum(PipelineRun.articles_new),
                   func.sum(PipelineRun.feeds_generated))
            .where(PipelineRun.created_at >= week_ago)
            .group_by(PipelineRun.run_type)
        )
        daily_pipeline_stats = [{"type": row[0], "count": row[1], "new_articles": row[2] or 0, "feeds": row[3] or 0} for row in r]

        result = {
            "stats": {"tenants": tc, "users": uc, "literature": lc, "notes": nc, "saved": sc, "wau_7d": wau},
            "data_quality": {
                "total": lc, "with_abstract": abstract_ok, "with_fulltext": fulltext_ok,
                "tagged": tagged, "with_pico": pico_ok, "with_study_design": design_ok,
                "with_cited_by": cited_ok, "with_doi": doi_ok,
            },
            "recent_pipelines": [{"type": r.run_type, "status": r.status, "new": r.articles_new, "feeds": r.feeds_generated,
                                   "completed": r.completed_at.strftime("%Y-%m-%dT%H:%M:%SZ") if r.completed_at else None} for r in recent_pipelines],
            "daily_pipeline_stats": daily_pipeline_stats,
        }
    await cache.set("admin:dashboard", result, ttl=300)
    return result


@router.get("/users/growth", response_model=dict, summary="用户增长")
async def user_growth():
    cached = await cache.get("admin:user_growth")
    if cached:
        return cached
    async with async_session() as db:
        from sqlalchemy import extract
        rows = await db.execute(
            select(
                extract("year", User.created_at).label("year"),
                extract("month", User.created_at).label("month"),
                func.count(User.id).label("cnt"),
            ).group_by("year", "month").order_by("year", "month")
        )
        result = {"growth": [{"year": int(y), "month": int(m), "count": c} for y, m, c in rows]}
    await cache.set("admin:user_growth", result, ttl=180)
    return result


@router.get("/stats/dau", response_model=dict, summary="日活跃用户趋势")
async def daily_active_users(
    days: int = Query(30, ge=7, le=365, description="统计最近N天"),
):
    key = f"admin:dau:{days}"
    cached = await cache.get(key)
    if cached:
        return cached
    from datetime import datetime, timedelta
    since = datetime.now(UTC).date() - timedelta(days=days - 1)
    async with async_session() as db:
        rows = await db.execute(
            select(
                func.date(LoginLog.created_at).label("day"),
                func.count(func.distinct(LoginLog.user_id)).label("dau"),
            )
            .where(LoginLog.created_at >= since)
            .group_by(text("day"))
            .order_by(text("day"))
        )
        result = {"dau": [{"date": str(d), "count": c} for d, c in rows], "days": days}
    await cache.set(key, result, ttl=180)
    return result


@router.get("/tags/distribution", response_model=dict, summary="标签分布")
async def tag_distribution():
    cached = await cache.get("admin:tag_distribution")
    if cached:
        return cached
    async with async_session() as db:
        tags = await db.execute(
            select(GlobalTag.tag_category, func.count(GlobalTag.id).label("cnt"),
                   func.sum(GlobalTag.article_count).label("articles"))
            .group_by(GlobalTag.tag_category).order_by(GlobalTag.tag_category)
        )
        result = {"categories": [{"category": c, "count": cnt, "articles": a or 0} for c, cnt, a in tags]}
    await cache.set("admin:tag_distribution", result, ttl=300)
    return result


# ─── 用户活动分析 ───

@router.get("/stats/page-views", response_model=dict, summary="页面访问趋势")
async def page_view_stats(
    days: int = Query(30, ge=7, le=365),
    _viewer: dict = Depends(require_platform_viewer),
):
    key = f"admin:page_views:{days}"
    cached = await cache.get(key)
    if cached:
        return cached
    from datetime import datetime, timedelta
    since = datetime.now(UTC).date() - timedelta(days=days - 1)
    async with async_session() as db:
        rows = await db.execute(
            select(
                func.date(UserActivityLog.created_at).label("day"),
                func.count(UserActivityLog.id).label("cnt"),
                func.count(func.distinct(UserActivityLog.user_id)).label("uv"),
            )
            .where(UserActivityLog.activity_type == "page_view")
            .where(UserActivityLog.created_at >= since)
            .group_by(text("day"))
            .order_by(text("day"))
        )
        result = {"page_views": [{"date": str(d), "pv": c, "uv": u} for d, c, u in rows], "days": days}
    await cache.set(key, result, ttl=180)
    return result


@router.get("/stats/anonymous-page-views", response_model=dict, summary="匿名页面访问趋势")
async def anonymous_page_view_stats(
    days: int = Query(7, ge=1, le=90),
    _viewer: dict = Depends(require_platform_viewer),
):
    """匿名访问 PV/UV 趋势（UV 按 anonymous_id 去重）"""
    key = f"admin:anon_page_views:{days}"
    cached = await cache.get(key)
    if cached:
        return cached
    from datetime import datetime, timedelta
    since = datetime.now(UTC).date() - timedelta(days=days - 1)
    async with async_session() as db:
        rows = await db.execute(
            select(
                func.date(AnonymousPageView.created_at).label("day"),
                func.count(AnonymousPageView.id).label("cnt"),
                func.count(func.distinct(AnonymousPageView.anonymous_id)).label("uv"),
            )
            .where(AnonymousPageView.created_at >= since)
            .group_by(text("day"))
            .order_by(text("day"))
        )
        result = {"page_views": [{"date": str(d), "pv": c, "uv": u} for d, c, u in rows], "days": days}
    await cache.set(key, result, ttl=180)
    return result


@router.get("/stats/behavior", response_model=dict, summary="用户行为分布")
async def behavior_stats(
    days: int = Query(7, ge=1, le=90),
    _viewer: dict = Depends(require_platform_viewer),
):
    key = f"admin:behavior:{days}"
    cached = await cache.get(key)
    if cached:
        return cached
    from datetime import datetime, timedelta
    since = datetime.now(UTC) - timedelta(days=days)
    async with async_session() as db:
        rows = await db.execute(
            select(
                UserActivityLog.activity_type,
                func.count(UserActivityLog.id).label("cnt"),
            )
            .where(UserActivityLog.created_at >= since)
            .group_by(UserActivityLog.activity_type)
            .order_by(text("cnt desc"))
        )
        result = {"behaviors": [{"type": t, "count": c} for t, c in rows], "days": days}
    await cache.set(key, result, ttl=180)
    return result


@router.get("/stats/popular-pages", response_model=dict, summary="热门页面")
async def popular_pages(
    days: int = Query(7, ge=1, le=90),
    limit: int = Query(20, ge=1, le=100),
    _viewer: dict = Depends(require_platform_viewer),
):
    key = f"admin:popular:{days}:{limit}"
    cached = await cache.get(key)
    if cached:
        return cached
    from datetime import datetime, timedelta
    since = datetime.now(UTC) - timedelta(days=days)
    async with async_session() as db:
        rows = await db.execute(
            select(
                UserActivityLog.target_type,
                UserActivityLog.target_id,
                func.count(UserActivityLog.id).label("cnt"),
            )
            .where(UserActivityLog.activity_type == "page_view")
            .where(UserActivityLog.created_at >= since)
            .group_by(UserActivityLog.target_type, UserActivityLog.target_id)
            .order_by(text("cnt desc"))
            .limit(limit)
        )
        result = {"pages": [{"type": t, "id": i, "count": c} for t, i, c in rows]}
    await cache.set(key, result, ttl=180)
    return result


# ─── 内容管理 ───

@router.get("/notifications", response_model=dict, summary="通知列表")
async def list_notifications(
    db: AsyncSession = Depends(get_db),
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=100),
):
    offset = (page - 1) * page_size
    total = (await db.execute(select(func.count(SystemNotification.id)))).scalar() or 0
    r = await db.execute(select(SystemNotification).order_by(SystemNotification.created_at.desc()).offset(offset).limit(page_size))
    ns = r.scalars().all()
    return {"total": total, "items": [{"id": str(n.id), "title": n.title, "content": n.content, "target_type": n.target_type,
                                       "created_at": n.created_at.isoformat() if n.created_at else None} for n in ns]}

@router.post("/notifications", response_model=dict, summary="创建通知")
async def create_notification(data: NotificationCreate, db: AsyncSession = Depends(get_db),
                              _op: dict = Depends(require_platform_operator)):
    n = SystemNotification(id=uuid.uuid4(), title=data.title, content=data.content,
                           target_type=data.target_type, target_id=data.target_id)
    db.add(n); await db.commit()
    return {"status": "created", "id": str(n.id)}

# ─── 标签引擎 ───

@router.get("/tags", response_model=dict, summary="标签管理")
async def admin_tags(
    db: AsyncSession = Depends(get_db),
    source: str = Query("", description="筛选来源: manual/mesh/auto"),
    active: str = Query("", description="筛选状态: true/false"),
    category: str = Query("", description="筛选类别: cancer/gene/treatment/..."),
    q: str = Query("", description="搜索: 中文名/英文名/类别"),
    page: int = Query(1, ge=1, description="页码"),
    page_size: int = Query(20, ge=10, le=500, description="每页条数"),
):
    conditions = []
    if source:
        conditions.append(GlobalTag.source == source)
    if active == "true":
        conditions.append(GlobalTag.is_active == True)
    elif active == "false":
        conditions.append(GlobalTag.is_active == False)
    if category:
        conditions.append(GlobalTag.tag_category == category)
    if q:
        conditions.append(
            GlobalTag.name_zh.ilike(f"%{q}%")
            | GlobalTag.name_en.ilike(f"%{q}%")
            | GlobalTag.tag_category.ilike(f"%{q}%")
        )

    total = await db.scalar(select(func.count(GlobalTag.id)).where(*conditions))

    q = select(GlobalTag).where(*conditions).order_by(
        GlobalTag.article_count.desc().nullslast(), GlobalTag.source, GlobalTag.tag_category
    ).offset((page - 1) * page_size).limit(page_size)
    r = await db.execute(q)
    tags = r.scalars().all()
    items = []
    for t in tags:
        items.append({
            "id": str(t.id), "name_zh": t.name_zh, "name_en": t.name_en,
            "path": t.path, "level": t.level, "tag_category": t.tag_category,
            "is_selectable": t.is_selectable, "article_count": t.article_count,
            "source": t.source, "is_active": t.is_active, "mesh_ui": t.mesh_ui,
        })
    return {"items": items, "total": total, "page": page, "page_size": page_size}


class TagUpdate(BaseModel):
    name_zh: str | None = None
    name_en: str | None = None
    tag_category: str | None = None
    is_selectable: bool | None = None
    is_active: bool | None = None


class TagCreate(BaseModel):
    name_zh: str | None = None
    name_en: str | None = None
    path: str = ""
    tag_category: str = "mesh_other"
    level: int = 1
    parent_id: str | None = None


@router.put("/tags/{tid}", response_model=dict, summary="更新标签")
async def update_tag(tid: str, data: TagUpdate, db: AsyncSession = Depends(get_db),
                     _op: dict = Depends(require_platform_operator)):
    try:
        tag_uuid = uuid.UUID(tid)
    except ValueError:
        raise HTTPException(400, "Invalid tag ID")
    t = (await db.execute(select(GlobalTag).where(GlobalTag.id == tag_uuid))).scalar()
    if not t: raise HTTPException(404)
    if data.name_zh is not None: t.name_zh = data.name_zh
    if data.name_en is not None: t.name_en = data.name_en
    if data.tag_category is not None: t.tag_category = data.tag_category
    if data.is_selectable is not None: t.is_selectable = data.is_selectable
    if data.is_active is not None: t.is_active = data.is_active
    if data.is_active == True and t.source == "auto":
        t.source = "manual"
    await db.commit()
    await cache.delete("public:tags")
    return {"status": "updated"}


@router.post("/tags", response_model=dict, summary="新建标签")
async def create_tag(data: TagCreate, db: AsyncSession = Depends(get_db),
                     _op: dict = Depends(require_platform_operator)):
    parent_uuid = uuid.UUID(data.parent_id) if data.parent_id else None
    tag = GlobalTag(
        name_zh=data.name_zh,
        name_en=data.name_en,
        path=data.path or data.name_zh or data.name_en or "",
        tag_category=data.tag_category,
        level=data.level,
        parent_id=parent_uuid,
        is_selectable=data.level <= 3,
        source="manual",
        is_active=True,
    )
    db.add(tag)
    await db.commit()
    await cache.delete("public:tags")
    return {"status": "created", "id": str(tag.id)}


@router.delete("/tags/{tid}", response_model=dict, summary="删除标签")
async def delete_tag(tid: str, db: AsyncSession = Depends(get_db),
                     _op: dict = Depends(require_platform_admin)):
    try:
        tag_uuid = uuid.UUID(tid)
    except ValueError:
        raise HTTPException(400, "Invalid tag ID")
    t = (await db.execute(select(GlobalTag).where(GlobalTag.id == tag_uuid))).scalar()
    if not t: raise HTTPException(404)
    # 先删除关联
    await db.execute(text("DELETE FROM global_literature_tags WHERE tag_id = :tid"), {"tid": tag_uuid})
    await db.execute(text("DELETE FROM global_tag_tree_numbers WHERE tag_id = :tid"), {"tid": tag_uuid})
    await db.execute(text("DELETE FROM user_subscriptions WHERE tag_id = :tid"), {"tid": tag_uuid})
    await db.execute(text("DELETE FROM user_dismissed_tags WHERE tag_id = :tid"), {"tid": tag_uuid})
    await db.delete(t)
    await db.commit()
    await cache.delete("public:tags")
    return {"status": "deleted"}


@router.post("/tags/refresh-counts", response_model=dict, summary="刷新标签文章计数")
async def refresh_tag_counts(db: AsyncSession = Depends(get_db),
                              _op: dict = Depends(require_platform_operator)):
    all_tags = (await db.execute(select(GlobalTag))).scalars().all()
    for tag in all_tags:
        cnt = await db.scalar(
            select(func.count(GlobalLiteratureTag.literature_id))
            .where(GlobalLiteratureTag.tag_id == tag.id)
        )
        tag.article_count = cnt or 0
    await db.commit()
    return {"status": "ok", "updated": len(all_tags)}

# ─── 期刊管理 ───

@router.get("/journals", response_model=dict, summary="期刊管理")
async def admin_journals(
    db: AsyncSession = Depends(get_db),
    page: int = Query(1, ge=1),
    page_size: int = Query(50, ge=1, le=200),
    q: str = Query(""),
):
    qs = select(GlobalJournal)
    if q:
        qs = qs.where(GlobalJournal.name.ilike(f"%{q}%"))
    total = (await db.execute(select(func.count()).select_from(qs.subquery()))).scalar() or 0
    r = await db.execute(qs.order_by(GlobalJournal.priority_score.desc().nullslast(), GlobalJournal.tier).offset((page-1)*page_size).limit(page_size))
    js = r.scalars().all()
    return {"items": [{"id": str(j.id), "name": j.name, "issn": j.issn, "tier": j.tier, "impact_factor": j.impact_factor,
                       "priority_score": j.priority_score, "priority_source": j.priority_source, "specialty": j.specialty} for j in js], "total": total, "page": page, "page_size": page_size}

class JournalUpdate(BaseModel):
    tier: str | None = None
    impact_factor: float | None = None
    priority_score: float | None = None
    priority_source: str | None = None
    specialty: str | None = None

@router.put("/journals/{jid}", response_model=dict, summary="更新期刊")
async def update_journal(jid: str, data: JournalUpdate, db: AsyncSession = Depends(get_db),
                         _op: dict = Depends(require_platform_operator)):
    from app.services.journal_utils import compute_priority_score, detect_specialty
    j = (await db.execute(select(GlobalJournal).where(GlobalJournal.id == uuid.UUID(jid)))).scalar()
    if not j: raise HTTPException(404)
    if data.tier is not None: j.tier = data.tier
    if data.impact_factor is not None: j.impact_factor = data.impact_factor
    if data.specialty is not None: j.specialty = data.specialty
    if data.priority_score is not None:
        j.priority_score = data.priority_score
        j.priority_source = "manual"
    elif data.priority_source == "auto":
        # 手动切回 auto → 重新计算
        j.priority_source = "auto"
        j.specialty = data.specialty or j.specialty or detect_specialty(j.tier, j.name)
        j.priority_score = compute_priority_score(j.tier, j.impact_factor, j.article_count, j.specialty)
    await db.commit()
    await cache.delete("public:journals")
    return {"status": "updated"}


@router.post("/journals/{from_id}/merge/{into_id}", summary="合并期刊")
async def merge_journal(from_id: str, into_id: str, db: AsyncSession = Depends(get_db),
                        _op: dict = Depends(require_platform_operator)):
    """将 from_id 的订阅迁入 into_id，删除 from_id。用于手工去重。"""
    from_j = (await db.execute(select(GlobalJournal).where(GlobalJournal.id == uuid.UUID(from_id)))).scalar()
    into_j = (await db.execute(select(GlobalJournal).where(GlobalJournal.id == uuid.UUID(into_id)))).scalar()
    if not from_j or not into_j:
        raise HTTPException(404, detail="期刊未找到")
    if from_j.id == into_j.id:
        raise HTTPException(400, detail="不能合并自身")

    # 迁移订阅（去重：同 user 已关注 into 的不重复 INSERT）
    await db.execute(
        text("""
            UPDATE user_journal_subscriptions
            SET journal_id = :into_id
            WHERE journal_id = :from_id
              AND NOT EXISTS (
                  SELECT 1 FROM user_journal_subscriptions
                  WHERE journal_id = :into_id AND user_id = user_journal_subscriptions.user_id
              )
        """),
        {"into_id": into_j.id, "from_id": from_j.id},
    )
    # 删除剩余重复订阅
    await db.execute(
        text("DELETE FROM user_journal_subscriptions WHERE journal_id = :from_id"),
        {"from_id": from_j.id},
    )

    # 更新文章数
    await db.execute(
        text("UPDATE global_journals SET article_count = article_count + :cnt, updated_at = NOW() WHERE id = :id"),
        {"cnt": from_j.article_count, "id": into_j.id},
    )

    # 删除被合并记录
    await db.execute(text("DELETE FROM global_journals WHERE id = :id"), {"id": from_j.id})
    await db.commit()
    await cache.delete("public:journals")
    return {"status": "merged", "from_name": from_j.name, "into_name": into_j.name}


# ─── 指南管理 ───

class GuidelineCreate(BaseModel):
    """新建指南版本（CSCO/卫健委等非 PubMed 来源）"""
    source: str  # CSCO / NHC
    cancer_type: str  # 癌种 tag name_en, 如 "Lung Cancer"
    version: str
    publish_date: str  # YYYY-MM-DD
    change_summary_zh: str | None = None
    source_url: str | None = None


@router.post("/guidelines", summary="新建指南版本")
async def admin_create_guideline(
    body: GuidelineCreate,
    db: AsyncSession = Depends(get_db),
    _op: dict = Depends(require_platform_operator)
):
    tag_r = await db.execute(
        select(GlobalTag).where(
            GlobalTag.tag_category == 'cancer',
            GlobalTag.is_active == True,
            GlobalTag.name_en == body.cancer_type,
        )
    )
    tag = tag_r.scalar_one_or_none()
    if not tag:
        # fallback ILIKE
        tag_r = await db.execute(
            select(GlobalTag).where(
                GlobalTag.tag_category == 'cancer',
                GlobalTag.is_active == True,
                GlobalTag.name_en.ilike(f'%{body.cancer_type}%'),
            )
        )
        tag = tag_r.scalar_one_or_none()
    if not tag:
        raise HTTPException(400, f"未找到癌种标签: {body.cancer_type}")

    try:
        pub_date = datetime.strptime(body.publish_date, "%Y-%m-%d").date()
    except ValueError:
        raise HTTPException(400, "日期格式错误，请使用 YYYY-MM-DD")

    gv = GuidelineVersion(
        guideline_source=body.source.upper(),
        cancer_type_tag_id=tag.id,
        version=body.version,
        publish_date=pub_date,
        change_summary_zh=body.change_summary_zh,
        source_url=body.source_url,
    )
    db.add(gv)
    await db.commit()

    # 通知关注该癌种的用户
    from app.services.cancer_notification import notify_new_guideline
    await notify_new_guideline(
        db, tag.id, body.source.upper(), body.version,
        tag.name_zh or tag.name_en, body.change_summary_zh,
    )

    return {"status": "created", "id": str(gv.id), "cancer_type_name": tag.name_zh or tag.name_en}


UPDATE_CADENCE: dict[str, dict] = {
    "CSCO": {"name_cn": "CSCO", "expected_update_month": 4, "interval_months": 14, "hint_cn": "CSCO 每年 3-5 月发布新版本，关注 csco.org.cn"},
    "NHC": {"name_cn": "卫健委", "expected_update_month": 1, "interval_months": 14, "hint_cn": "卫健委每年 1 月左右发布新版诊疗指南，关注 nhc.gov.cn"},
    "NCCN": {"name_cn": "NCCN", "expected_update_month": None, "interval_months": 6, "hint_cn": "NCCN 持续更新，PubMed 自动捕获无需手动维护"},
    "ESMO": {"name_cn": "ESMO", "expected_update_month": None, "interval_months": 12, "hint_cn": "ESMO 不定期更新，PubMed 自动捕获无需手动维护"},
    "ASTRO": {"name_cn": "ASTRO", "expected_update_month": None, "interval_months": 18, "hint_cn": "ASTRO 不定期更新，PubMed 自动捕获无需手动维护"},
}


@router.get("/guidelines/check-updates", summary="检查指南更新状态")
async def admin_check_guideline_updates(db: AsyncSession = Depends(get_db)):
    today = datetime.utcnow().date()
    current_year = today.year
    current_month = today.month

    # 获取各来源最新版本
    r = await db.execute(
        select(
            GuidelineVersion.guideline_source,
            func.max(GuidelineVersion.publish_date).label("latest_date"),
            func.count(GuidelineVersion.id).label("total"),
        )
        .group_by(GuidelineVersion.guideline_source)
    )
    by_source = {row.guideline_source: {"latest_date": row.latest_date, "total": row.total} for row in r}

    result = {}
    for src, info in UPDATE_CADENCE.items():
        src_data = by_source.get(src)
        if not src_data:
            result[src] = {"status": "no_data", "hint": info["hint_cn"], "total": 0, "latest_date": None}
            continue

        latest = src_data["latest_date"]
        months_since = (today.year - latest.year) * 12 + (today.month - latest.month)

        if info["expected_update_month"]:
            # 有固定更新月份: 看当年是否有新版
            year_has_new = await db.execute(
                select(func.count(GuidelineVersion.id))
                .where(
                    GuidelineVersion.guideline_source == src,
                    func.extract('year', GuidelineVersion.publish_date) == current_year,
                )
            )
            count_current_year = year_has_new.scalar() or 0
            # 如果当前月份已过预期更新月，但当年没有新版本 → stale
            expected = info["expected_update_month"]
            is_stale = current_month > expected and count_current_year == 0
            result[src] = {
                "status": "stale" if is_stale else "ok",
                "total": src_data["total"],
                "latest_date": str(latest) if latest else None,
                "year_has_new": count_current_year > 0,
            }
        else:
            # 无固定更新月份: 看距离上次是否超过 interval
            is_stale = months_since > info["interval_months"]
            result[src] = {
                "status": "stale" if is_stale else "ok",
                "total": src_data["total"],
                "latest_date": str(latest) if latest else None,
            }

        result[src]["hint"] = info["hint_cn"]

    return {"sources": result, "check_date": str(today)}


# ─── 指南证据管理 ───

class EvidenceCreate(BaseModel):
    literature_id: str
    recommendation: str | None = None
    recommendation_level: str | None = None
    treatment_line: str | None = None


class EvidenceUpdate(BaseModel):
    recommendation: str | None = None
    recommendation_level: str | None = None
    treatment_line: str | None = None


@router.get("/guidelines", summary="指南列表（管理）")
async def admin_guidelines(db: AsyncSession = Depends(get_db)):
    r = await db.execute(
        select(
            GuidelineVersion,
            func.count(GuidelineEvidence.id).label("ev_count"),
            GlobalTag.name_zh,
        )
        .outerjoin(GuidelineEvidence, GuidelineEvidence.guideline_version_id == GuidelineVersion.id)
        .outerjoin(GlobalTag, GlobalTag.id == GuidelineVersion.cancer_type_tag_id)
        .group_by(GuidelineVersion.id, GlobalTag.name_zh)
        .order_by(GuidelineVersion.guideline_source, GuidelineVersion.publish_date.desc())
    )
    return {
        "items": [
            {
                "id": str(gv.id),
                "source": gv.guideline_source,
                "version": gv.version,
                "cancer_type": tag_name or "",
                "publish_date": str(gv.publish_date) if gv.publish_date else None,
                "evidence_count": ev_count,
            }
            for gv, ev_count, tag_name in r
        ]
    }


@router.get("/guidelines/{gid}/evidence", summary="指南证据列表（管理）")
async def admin_evidence_list(gid: str, db: AsyncSession = Depends(get_db)):
    try:
        gid_uuid = uuid.UUID(gid)
    except ValueError:
        raise HTTPException(400, "Invalid guideline version ID")
    gv = await db.get(GuidelineVersion, gid_uuid)
    if not gv:
        raise HTTPException(404, "Guideline version not found")
    result = await db.execute(
        select(GuidelineEvidence, GlobalLiterature.title, GlobalLiterature.pmid, GlobalLiterature.journal)
        .join(GlobalLiterature, GuidelineEvidence.literature_id == GlobalLiterature.id)
        .where(GuidelineEvidence.guideline_version_id == gid_uuid)
        .order_by(GuidelineEvidence.recommendation_level.asc())
    )
    return {
        "guideline": {
            "id": str(gv.id),
            "source": gv.guideline_source,
            "version": gv.version,
        },
        "items": [
            {
                "id": str(ev.id),
                "literature_id": str(ev.literature_id),
                "recommendation": ev.recommendation,
                "level": ev.recommendation_level,
                "treatment_line": ev.treatment_line,
                "pmid": pmid,
                "title": title[:120] if title else "",
                "journal": journal,
            }
            for ev, title, pmid, journal in result
        ],
    }


@router.post("/guidelines/{gid}/evidence", summary="添加证据")
async def admin_create_evidence(
    gid: str, body: EvidenceCreate, db: AsyncSession = Depends(get_db),
    _op: dict = Depends(require_platform_operator)
):
    from app.models.business import GuidelineEvidence, validate_treatment_line
    try:
        gid_uuid = uuid.UUID(gid)
        lit_uuid = uuid.UUID(body.literature_id)
    except ValueError:
        raise HTTPException(400, "Invalid UUID")
    gv = await db.get(GuidelineVersion, gid_uuid)
    if not gv:
        raise HTTPException(404, "Guideline version not found")
    lit = await db.get(GlobalLiterature, lit_uuid)
    if not lit:
        raise HTTPException(404, "Literature not found")
    tl = validate_treatment_line(body.treatment_line)
    ev = GuidelineEvidence(
        guideline_version_id=gid_uuid,
        literature_id=lit_uuid,
        recommendation=body.recommendation,
        recommendation_level=body.recommendation_level,
        treatment_line=tl,
    )
    db.add(ev)
    await db.commit()
    return {"status": "created", "id": str(ev.id)}


@router.put("/evidence/{eid}", summary="更新证据")
async def admin_update_evidence(
    eid: str, body: EvidenceUpdate, db: AsyncSession = Depends(get_db),
    _op: dict = Depends(require_platform_operator)
):
    from app.models.business import GuidelineEvidence, validate_treatment_line
    try:
        eid_uuid = uuid.UUID(eid)
    except ValueError:
        raise HTTPException(400, "Invalid evidence ID")
    ev = await db.get(GuidelineEvidence, eid_uuid)
    if not ev:
        raise HTTPException(404, "Evidence not found")
    if body.recommendation is not None:
        ev.recommendation = body.recommendation
    if body.recommendation_level is not None:
        ev.recommendation_level = body.recommendation_level
    if body.treatment_line is not None:
        ev.treatment_line = validate_treatment_line(body.treatment_line)
    await db.commit()
    return {"status": "updated"}


@router.delete("/evidence/{eid}", summary="删除证据")
async def admin_delete_evidence(eid: str, db: AsyncSession = Depends(get_db),
                                _op: dict = Depends(require_platform_operator)):
    from app.models.business import GuidelineEvidence
    try:
        eid_uuid = uuid.UUID(eid)
    except ValueError:
        raise HTTPException(400, "Invalid evidence ID")
    ev = await db.get(GuidelineEvidence, eid_uuid)
    if not ev:
        raise HTTPException(404, "Evidence not found")
    await db.delete(ev)
    await db.commit()
    return {"status": "deleted"}


# ─── 系统配置（只读） ───

@router.get("/config", response_model=dict, summary="系统配置")
async def get_config():
    from app.config import settings
    return {"specialty": settings.SPECIALTY, "debug": settings.DEBUG, "jwt_expire_min": settings.ACCESS_TOKEN_EXPIRE_MINUTES}


class AiConfigUpdate(BaseModel):
    provider: str | None = None
    api_key: str | None = None
    base_url: str | None = None
    model_name: str | None = None
    temperature: int | None = None  # 0-10, display as 0.0-1.0
    is_active: bool | None = None


@router.get("/ai-config", response_model=dict, summary="AI 配置")
async def get_ai_config(db: AsyncSession = Depends(get_db)):
    cfg = (await db.execute(
        select(AiProviderConfig).where(AiProviderConfig.is_active == True)
        .order_by(AiProviderConfig.created_at.desc()).limit(1)
    )).scalar()
    if not cfg:
        return {"configured": False}
    key_masked = f"****...{cfg.api_key[-2:]} (len {len(cfg.api_key)})" if len(cfg.api_key) > 2 else "****"
    return {
        "configured": True, "id": str(cfg.id),
        "provider": cfg.provider, "api_key": key_masked,
        "base_url": cfg.base_url, "model_name": cfg.model_name,
        "temperature": cfg.temperature, "is_active": cfg.is_active,
        "updated_at": cfg.updated_at.isoformat() if cfg.updated_at else None,
    }


@router.put("/ai-config", response_model=dict, summary="更新 AI 配置")
async def update_ai_config(data: AiConfigUpdate, db: AsyncSession = Depends(get_db),
                           _admin: dict = Depends(require_platform_admin)):
    cfg = (await db.execute(
        select(AiProviderConfig).where(AiProviderConfig.is_active == True)
        .order_by(AiProviderConfig.created_at.desc()).limit(1)
    )).scalar()
    if not cfg:
        cfg = AiProviderConfig(
            provider=data.provider or "deepseek",
            api_key=data.api_key or "",
            base_url=data.base_url or "",
            model_name=data.model_name or "",
            is_active=True,
        )
        db.add(cfg)
    if data.provider is not None: cfg.provider = data.provider
    if data.api_key: cfg.api_key = data.api_key  # 空字符串不覆盖
    if data.base_url is not None: cfg.base_url = data.base_url
    if data.model_name is not None: cfg.model_name = data.model_name
    if data.temperature is not None: cfg.temperature = data.temperature
    if data.is_active is not None: cfg.is_active = data.is_active
    cfg.updated_by = uuid.UUID(_admin["sub"])
    await db.commit()
    return {"status": "updated", "provider": cfg.provider}


@router.get("/pipeline/runs", response_model=dict, summary="流水线记录")
async def pipeline_runs(
    db: AsyncSession = Depends(get_db),
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=100),
):
    offset = (page - 1) * page_size
    total = (await db.execute(select(func.count(PipelineRun.id)))).scalar() or 0
    r = await db.execute(select(PipelineRun).order_by(PipelineRun.created_at.desc()).offset(offset).limit(page_size))
    runs = r.scalars().all()
    return {"total": total, "items": [{"id": str(p.id), "type": p.run_type, "status": p.status, "new": p.articles_new,
                                       "filtered": p.articles_filtered, "feeds": p.feeds_generated, "error": p.error_log,
                                       "started": p.started_at.strftime("%Y-%m-%dT%H:%M:%SZ") if p.started_at else None,
                                       "completed": p.completed_at.strftime("%Y-%m-%dT%H:%M:%SZ") if p.completed_at else None} for p in runs]}


@router.post("/pipeline/refresh-citations", response_model=dict, summary="刷新引用次数")
async def refresh_citations(
    db: AsyncSession = Depends(get_db),
    limit: int = Query(0, ge=0, description="0=全库，N=最新N篇"),
    _op: dict = Depends(require_platform_operator),
):
    """批量更新文献被引次数（PubMed elink，免费）"""
    from app.services.pubmed_api import update_citation_counts

    result = await update_citation_counts(db, limit=limit)
    return {
        "status": "ok",
        "updated": result["updated"],
        "total": result["total"],
        "errors": result["errors"],
        "note": "引用数据来自 PubMed elink (pubmed_pubmed_citedin)，每日更新。",
    }


# ─── 新药审批同步 ───

@router.post("/pipeline/sync-drug-approvals", response_model=dict, summary="同步 FDA 新药审批数据")
async def sync_drug_approvals(
    mode: str = Query("full", pattern="^(full|recent)$",
                      description="full=全量扫描FDA标签, recent=仅最近90天新药"),
    _op: dict = Depends(require_platform_operator),
):
    """从 FDA openFDA API 同步肿瘤新药审批数据

    数据流：
    FDA Label API (按 indications_and_usage 搜索肿瘤关键词)
    → FDA Drugs@FDA API (获取审批详情)
    → 写入 drug_approvals 表

    FDA 官方 API，完全开放，无需 API Key。
    参考：https://open.fda.gov/apis/drug/drugsfda/
    """
    from app.services.drug_approval_sync import sync_all_fda_approvals, sync_recent_fda_approvals

    if mode == "recent":
        result = await sync_recent_fda_approvals()
    else:
        result = await sync_all_fda_approvals()

    return {"status": "ok", "mode": mode, **result}


@router.post("/pipeline/sync-drug-labels", response_model=dict, summary="同步 FDA 药品标签")
async def sync_drug_labels(
    _op: dict = Depends(require_platform_operator),
):
    """从 FDA Label API 获取适应症原文、临床试验描述、作用机制"""
    from app.services.drug_label_sync import sync_drug_labels as _sync

    result = await _sync()
    return {"status": "ok", **result}


@router.post("/pipeline/parse-indications", response_model=dict, summary="解析 FDA Label 适应症")
async def parse_indications(
    _op: dict = Depends(require_platform_operator),
):
    """从 DrugLabel 适应症文本中提取癌种、治疗线数、联合方案"""
    from app.services.indication_parser import parse_all_labels

    result = await parse_all_labels()
    return {"status": "ok", **result}


# ─── 新药审批管理 CRUD ───


class DrugApprovalCreate(BaseModel):
    drug_name: str
    generic_name: str
    target: str | None = None
    indication: str | None = None
    approval_agency: str = "NMPA"
    approval_type: str | None = "new_drug"
    approval_date: str  # YYYY-MM-DD
    source_url: str | None = None
    pivotal_trial_nct: str | None = None
    pivotal_trial_pmid: int | None = None
    cancer_type_tag_id: str | None = None


class DrugApprovalUpdate(BaseModel):
    drug_name: str | None = None
    generic_name: str | None = None
    target: str | None = None
    indication: str | None = None
    approval_type: str | None = None
    approval_date: str | None = None
    source_url: str | None = None


@router.get("/drug-approvals", summary="审批管理列表")
async def admin_list_approvals(
    page: int = Query(1, ge=1),
    page_size: int = Query(50, ge=1, le=200),
    target_null: bool = Query(False, description="仅显示 target 为空"),
    agency: str = Query(None),
    db: AsyncSession = Depends(get_db),
    _viewer: dict = Depends(require_platform_viewer),
):
    """管理员查看/管理所有审批记录"""
    from datetime import date

    offset = (page - 1) * page_size
    q = select(DrugApproval).order_by(DrugApproval.approval_date.desc())
    count_q = select(func.count(DrugApproval.id))
    if target_null:
        q = q.where(DrugApproval.target.is_(None))
        count_q = count_q.where(DrugApproval.target.is_(None))
    if agency:
        q = q.where(DrugApproval.approval_agency == agency.upper())
        count_q = count_q.where(DrugApproval.approval_agency == agency.upper())
    total = (await db.execute(count_q)).scalar() or 0
    result = await db.execute(q.offset(offset).limit(page_size))
    items = result.scalars().all()
    return {
        "items": [{"id": str(a.id), "drug_name": a.drug_name, "generic_name": a.generic_name,
                    "target": a.target, "indication": a.indication, "approval_agency": a.approval_agency,
                    "approval_date": a.approval_date.isoformat() if a.approval_date else None,
                    "approval_type": a.approval_type, "source_url": a.source_url} for a in items],
        "total": total,
    }


@router.post("/drug-approvals", summary="新增审批记录")
async def admin_create_approval(
    req: DrugApprovalCreate,
    db: AsyncSession = Depends(get_db),
    _op: dict = Depends(require_platform_operator),
):
    """手动新增药品审批（用于 NMPA 等非 FDA 数据）"""
    from datetime import date
    from app.db import new_uuid

    try:
        d = date.fromisoformat(req.approval_date)
    except ValueError:
        raise HTTPException(400, "日期格式无效，请用 YYYY-MM-DD")

    # 解析 cancer_type_tag_id
    ctid = None
    cancer_name = None
    if req.cancer_type_tag_id:
        try:
            ctid = uuid.UUID(req.cancer_type_tag_id)
            tag = await db.get(GlobalTag, ctid)
            if tag:
                cancer_name = tag.name_zh or tag.name_en
        except ValueError:
            pass

    da = DrugApproval(
        id=new_uuid(),
        drug_name=req.drug_name[:500],
        generic_name=(req.generic_name or "")[:500],
        target=req.target[:200] if req.target else None,
        indication=req.indication or f"{req.drug_name} 审批",
        cancer_type_tag_id=ctid,
        approval_agency=req.approval_agency.upper(),
        approval_type=req.approval_type,
        approval_date=d,
        source_url=req.source_url[:1000] if req.source_url else None,
        pivotal_trial_nct=req.pivotal_trial_nct,
        pivotal_trial_pmid=req.pivotal_trial_pmid,
    )
    db.add(da)
    await db.commit()

    # 通知关注该癌种的用户
    if ctid:
        from app.services.cancer_notification import notify_new_drug_approval
        await notify_new_drug_approval(
            db, ctid, req.drug_name, req.target or "", req.indication or "",
        )

    return {"id": str(da.id), "status": "created"}


@router.put("/drug-approvals/{aid}", summary="更新审批记录")
async def admin_update_approval(
    aid: str,
    req: DrugApprovalUpdate,
    db: AsyncSession = Depends(get_db),
    _op: dict = Depends(require_platform_operator),
):
    """更新审批记录（如修复 target）"""
    from datetime import date
    try:
        da = await db.get(DrugApproval, uuid.UUID(aid))
    except ValueError:
        raise HTTPException(400, "无效 ID")
    if not da:
        raise HTTPException(404)

    if req.drug_name is not None:
        da.drug_name = req.drug_name[:500]
    if req.generic_name is not None:
        da.generic_name = req.generic_name[:500]
    if req.target is not None:
        da.target = req.target[:200] if req.target else None
    if req.indication is not None:
        da.indication = req.indication
    if req.approval_type is not None:
        da.approval_type = req.approval_type
    if req.approval_date is not None:
        try:
            da.approval_date = date.fromisoformat(req.approval_date)
        except ValueError:
            raise HTTPException(400, "日期格式无效")
    if req.source_url is not None:
        da.source_url = req.source_url[:1000] if req.source_url else None
    await db.commit()
    return {"id": aid, "status": "updated"}


@router.delete("/drug-approvals/{aid}", summary="删除审批记录")
async def admin_delete_approval(
    aid: str,
    db: AsyncSession = Depends(get_db),
    _op: dict = Depends(require_platform_operator),
):
    """删除审批记录"""
    try:
        da = await db.get(DrugApproval, uuid.UUID(aid))
    except ValueError:
        raise HTTPException(400, "无效 ID")
    if not da:
        raise HTTPException(404)
    await db.delete(da)
    await db.commit()
    return {"status": "deleted"}


# ─── AI 摘要 ───

@router.post("/pipeline/extract-pico", response_model=dict, summary="批量提取 PICO")
async def extract_pico_batch(
    db: AsyncSession = Depends(get_db),
    limit: int = Query(100, ge=1, le=1000, description="本次处理篇数上限"),
    _op: dict = Depends(require_platform_operator),
):
    """批量从文献摘要 + 全文提取 PICO 要素（按 journal tier + cited_by_count 排序）"""
    from app.services.ai_summary import extract_pico, MAX_CONCURRENT

    result = await db.execute(
        select(GlobalLiterature)
        .where(
            GlobalLiterature.pico.is_(None),
            (GlobalLiterature.abstract.isnot(None)) | (GlobalLiterature.full_text_sections.isnot(None)) | (GlobalLiterature.full_text_path.isnot(None)),
        )
        .order_by(GlobalLiterature.cited_by_count.desc())
        .limit(limit)
    )
    articles = result.scalars().all()
    if not articles:
        return {"status": "ok", "processed": 0, "total": 0}

    import asyncio
    sem = asyncio.Semaphore(MAX_CONCURRENT)
    done = 0

    async def process(lit: GlobalLiterature) -> int:
        async with sem:
            ft = await get_full_text(lit)
            pico = await extract_pico(
                lit.title,
                lit.abstract,
                ft,
                db=db,
            )
            if pico:
                lit.pico = pico
                return 1
            return 0

    tasks = [process(lit) for lit in articles]
    results = await asyncio.gather(*tasks)
    done = sum(results)
    await db.commit()
    return {"status": "ok", "processed": done, "total": len(articles)}


@router.post("/pipeline/backfill-study-designs", response_model=dict, summary="回填研究设计分类")
async def backfill_study_designs(
    db: AsyncSession = Depends(get_db),
    _op: dict = Depends(require_platform_operator),
):
    """为所有有 pub_types 但无 study_design 的文献回填研究设计分类"""
    from app.services.study_classifier import backfill_all_designs

    updated = await backfill_all_designs(db)
    return {"status": "ok", "updated": updated}


@router.post("/pipeline/backfill-oa-text", response_model=dict, summary="回填 PMC OA 全文")
async def backfill_oa_fulltext(
    db: AsyncSession = Depends(get_db),
    limit: int = Query(500, ge=1, le=5000, description="本次处理篇数上限"),
    _op: dict = Depends(require_platform_operator),
):
    """为有 PMC 版本但无全文的文献回填 OA 全文

    流程：elink 查 PMC → efetch 抓 JATS XML → 解析 sections/tables/refs → 入库
    """
    import asyncio
    import logging
    from xml.etree import ElementTree as ET

    import httpx

    from app.config import settings
    from app.services.jats_parser import extract_baseline_data, parse_full_text
    from app.services.pmc_oa import fetch_pmc_xml

    logger = logging.getLogger(__name__)

    # 1. 选出无全文的文献（按引用数排序，优先高价值文献）
    result = await db.execute(
        select(GlobalLiterature)
        .where(GlobalLiterature.full_text_sections.is_(None), GlobalLiterature.full_text_path.is_(None))
        .order_by(GlobalLiterature.cited_by_count.desc().nullslast())
        .limit(limit)
    )
    articles = result.scalars().all()
    if not articles:
        return {"status": "ok", "found_pmc": 0, "fetched": 0, "total": 0}

    total = len(articles)
    pmids = [a.pmid for a in articles if a.pmid]

    # 2. elink 批量查询 PMID → PMC ID
    ELINK_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/elink.fcgi"
    api_key = settings.PUBMED_API_KEY or None
    elink_delay = 0.35 if not api_key else 0.11

    pmc_map: dict[int, str] = {}  # pmid → pmc_numeric_id

    async with httpx.AsyncClient(timeout=30.0) as client:
        for i in range(0, len(pmids), 200):
            batch = [str(p) for p in pmids[i:i + 200]]
            params = {
                "dbfrom": "pubmed",
                "db": "pmc",
                "id": ",".join(batch),
                "retmode": "xml",
                "tool": "scilit_oncology",
                "email": "dev@scilit-oncology.com",
            }
            if api_key:
                params["api_key"] = api_key
            try:
                r = await client.get(ELINK_URL, params=params, timeout=30.0)
                r.raise_for_status()
                root = ET.fromstring(r.text)
                for linkset in root.findall(".//LinkSet"):
                    id_elem = linkset.find(".//IdList/Id")
                    if id_elem is None:
                        continue
                    pmid = int(id_elem.text)
                    pmc_elem = linkset.find(".//LinkSetDb/Link/Id")
                    if pmc_elem is not None:
                        pmc_map[pmid] = pmc_elem.text
            except Exception as e:
                logger.warning("oa_backfill_elink_error offset=%d: %s", i, e)
            await asyncio.sleep(elink_delay)

    found = len(pmc_map)
    if not found:
        return {"status": "ok", "found_pmc": 0, "fetched": 0, "total": total,
                "note": "未找到有 PMC 版本的文献"}

    # 3. 抓取并解析 OA 全文
    art_by_pmid = {a.pmid: a for a in articles}
    ok = 0

    for pmid, pmc_numeric in pmc_map.items():
        lit = art_by_pmid.get(pmid)
        if lit is None:
            continue

        xml = await fetch_pmc_xml(pmc_numeric)
        if not xml:
            continue

        try:
            parsed = parse_full_text(xml)
            if not parsed:
                lit.pre_extracted_data = {
                    "_extract_failed": True,
                    "_extract_error": "XML 解析为空",
                }
                continue

            if bool(settings.COS_SECRET_ID):
                key = f"fulltext/{lit.pmid}.json"
                if await upload_json(key, parsed):
                    lit.full_text_path = key
                else:
                    lit.full_text_sections = parsed
            else:
                lit.full_text_sections = parsed
            lic = parsed.get("license")
            if lic:
                lit.license_info = lic
            lit.pmc_id = pmc_numeric
            lit.is_oa = True

            baseline = extract_baseline_data(parsed)
            if baseline:
                lit.pre_extracted_data = baseline
            else:
                lit.pre_extracted_data = {
                    "_extract_failed": False,
                    "_note": "未识别到基线特征表",
                }
            ok += 1
        except Exception:
            logger.warning("oa_backfill_parse_failed pmid=%s pmc=%s", pmid, pmc_numeric, exc_info=True)

    if ok:
        await db.commit()
        # 全文已重新存储 → 失效缓存
        for pmid in pmc_map:
            await invalidate_full_text_cache(pmid)

    logger.info("oa_backfill_done: %d of %d have PMC, %d fetched", found, total, ok)
    return {"status": "ok", "found_pmc": found, "fetched": ok, "total": total}


@router.post("/pipeline/retag-tags", response_model=dict, summary="回填 MeSH 标签")
async def retag_tags(
    db: AsyncSession = Depends(get_db),
    limit: int = Query(5000, ge=1, le=50000, description="本次处理篇数上限"),
    _op: dict = Depends(require_platform_operator),
):
    """为已有 mesh_headings 但缺少 GlobalTag 关联的文献回填 MeSH 标签

    利用已有 mesh_headings 中的 MeSH UI 与 GlobalTag.mesh_ui 匹配，
    自动创建 GlobalLiteratureTag 关联。新增文献已由管道自动打标，
    此接口仅用于回填管道上线前已存在的文献。
    """
    import logging
    from app.services.pubmed_api import _tag_article

    logger = logging.getLogger(__name__)

    # 1. 选出有 mesh_headings 但可能缺标签的文献
    subq = select(GlobalLiteratureTag.literature_id).distinct().subquery()
    result = await db.execute(
        select(GlobalLiterature)
        .where(
            GlobalLiterature.mesh_headings.isnot(None),
            ~GlobalLiterature.id.in_(select(subq)),
        )
        .limit(limit)
    )
    articles = result.scalars().all()
    if not articles:
        return {"status": "ok", "total_articles": 0, "tagged_articles": 0, "tags_added": 0}

    total = len(articles)
    tagged_count = 0
    tags_added = 0

    for lit in articles:
        mh = lit.mesh_headings
        if not mh or (isinstance(mh, list) and len(mh) == 0):
            continue
        n = await _tag_article(db, lit.id, mh)
        if n:
            tagged_count += 1
            tags_added += n

    if tags_added:
        await db.commit()

    logger.info("retag_tags_done: %d articles processed, %d tagged (%d tags added)", total, tagged_count, tags_added)
    return {"status": "ok", "total_articles": total, "tagged_articles": tagged_count, "tags_added": tags_added}


@router.post("/pipeline/run", response_model=dict, summary="运行 PubMed 检索流水线")
async def run_pipeline(
    db: AsyncSession = Depends(get_db),
    mode: str = Query("full", pattern="^(full|majr|broad)$", description="full=全量, majr=仅精搜, broad=仅宽搜"),
    max_per_query: int = Query(30, ge=5, le=5000, description="每查询最多结果数（Europe PMC 支持到 5000）"),
    precision_mode: str = Query("majr", pattern="^(majr|mesh)$", description="majr=Major Topic优先, mesh=MeSH更高召回"),
    source: str = Query("ncbi", pattern="^(ncbi|europe_pmc)$", description="ncbi=NCBI E-utilities, europe_pmc=Europe PMC API"),
    _op: dict = Depends(require_platform_operator),
):
    """运行肿瘤学文献检索流水线，支持精度模式、搜索范围控制和数据源切换。"""
    from app.services.pubmed_api import _run_pipeline

    use_majr = mode in ("full", "majr")
    use_broad = mode in ("full", "broad")
    stats = await _run_pipeline(
        db, max_per_query,
        use_majr=use_majr, use_broad=use_broad,
        precision_mode=precision_mode,
        use_europe_pmc=(source == "europe_pmc"),
    )

    # 记录运行日志
    from datetime import datetime
    from app.compat import UTC
    run_type = f"manual_{mode}"
    if source == "europe_pmc":
        run_type += "_epmc"
    run_log = PipelineRun(
        id=uuid.uuid4(),
        run_type=run_type,
        status="success",
        articles_total=stats["fetched"],
        articles_new=stats["new"],
        articles_filtered=stats["tagged"],
        feeds_generated=stats["feeds"],
        started_at=datetime.now(UTC),
        completed_at=datetime.now(UTC),
    )
    db.add(run_log)
    await db.commit()

    # 刷新 stats 缓存
    try:
        from app.api.v1.public import refresh_stats_cache
        await refresh_stats_cache()
    except Exception:
        pass

    return {"status": "ok", "mode": mode, "precision": precision_mode, **stats}


@router.post("/ai/summarize", response_model=dict, summary="生成摘要")
async def trigger_ai_summary(
    db: AsyncSession = Depends(get_db),
    limit: int = Query(0, ge=0, le=5000, description="0=全库无摘要文献"),
    _op: dict = Depends(require_platform_operator),
):
    """批量生成 AI 一句话摘要（对尚无 one_liner 的文献）"""
    from app.services.ai_summary import generate_one_liner
    q = select(GlobalLiterature).where(GlobalLiterature.abstract.isnot(None), GlobalLiterature.abstract != "")
    if limit > 0:
        q = q.order_by(GlobalLiterature.pub_date.desc().nullslast()).limit(limit)
    else:
        q = q.order_by(GlobalLiterature.pub_date.desc().nullslast())
    result = await db.execute(q)
    articles = result.scalars().all()
    done = 0
    for lit in articles:
        curr = lit.ai_summary or {}
        if curr.get("one_liner"):
            continue  # 跳过已有摘要的
        summary = await generate_one_liner(lit.title, lit.abstract, db=db)
        if summary:
            curr["one_liner"] = summary
            await db.commit()
    return {"status": "ok", "processed": done, "total": len(articles)}


# ─── 网站访问记录 ───

@router.get("/page-views", response_model=dict, summary="页面访问记录")
async def page_views(
    db: AsyncSession = Depends(get_db),
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=100),
    ip: str = Query("", description="IP 模糊搜索"),
    # location: str = Query("", description="所在地模糊搜索"),  # 已注释
    start_date: str = Query("", description="起始日期 YYYY-MM-DD"),
    end_date: str = Query("", description="截止日期 YYYY-MM-DD"),
):
    from datetime import datetime, timedelta

    # 默认最近 30 天，避免全表扫描
    _now = datetime.now(UTC)
    _default_start = (_now - timedelta(days=30)).strftime("%Y-%m-%d")
    _default_end = _now.strftime("%Y-%m-%d")
    sd_str = start_date or _default_start
    ed_str = end_date or _default_end

    q = (
        select(UserActivityLog, User.display_name)
        .outerjoin(User, UserActivityLog.user_id == User.id)
        .where(UserActivityLog.activity_type == "page_view")
    )
    count_q = (
        select(func.count(UserActivityLog.id))
        .where(UserActivityLog.activity_type == "page_view")
    )

    if ip:
        q = q.where(UserActivityLog.ip_address.ilike(f"%{ip}%"))
        count_q = count_q.where(UserActivityLog.ip_address.ilike(f"%{ip}%"))
    # 始终应用日期范围（默认最近 30 天），避免全表扫描
    try:
        sd = datetime.strptime(sd_str, "%Y-%m-%d")
        q = q.where(UserActivityLog.created_at >= sd)
        count_q = count_q.where(UserActivityLog.created_at >= sd)
    except ValueError:
        pass
    try:
        ed = datetime.strptime(ed_str, "%Y-%m-%d") + timedelta(days=1)
        q = q.where(UserActivityLog.created_at < ed)
        count_q = count_q.where(UserActivityLog.created_at < ed)
    except ValueError:
        pass

    total = (await db.execute(count_q)).scalar() or 0
    offset = (page - 1) * page_size
    result = await db.execute(
        q.order_by(UserActivityLog.created_at.desc())
        .offset(offset)
        .limit(page_size)
    )
    rows = result.all()

    # 收集当前页所有 IP（去重）并行查询地理位置
    ips = {row[0].ip_address for row in rows if row[0].ip_address}
    geo_map: dict[str, dict | None] = {}
    if ips:
        import asyncio
        tasks = {ip: lookup_ip(ip) for ip in ips}
        results = await asyncio.gather(*tasks.values())
        geo_map = dict(zip(tasks.keys(), results))

    items = []
    for log, display_name in rows:
        loc = geo_map.get(log.ip_address) if log.ip_address else None
        detail = log.detail or {}

        # # 过滤所在地（在 Python 层执行） 已注释
        # if location and loc:
        #     loc_str = f"{loc.get('city','')} {loc.get('regionName','')} {loc.get('country','')}".lower()
        #     if location.lower() not in loc_str:
        #         continue

        items.append({
            "id": str(log.id),
            "user_id": str(log.user_id) if log.user_id else None,
            "display_name": display_name,
            "ip_address": log.ip_address,
            # "location": loc,  # 已注释
            "path": detail.get("path"),
            "page_title": detail.get("title"),
            "duration_seconds": log.duration_seconds,
            "user_agent": log.user_agent,
            "created_at": log.created_at.isoformat() if log.created_at else None,
        })

    # # 如果 location 过滤生效，重新计算 total（仅当前页，不重新查库） 已注释
    # filtered_total = total if not location else len(items) + max(0, total - offset - len(rows))

    return {"items": items, "total": total, "page": page, "page_size": page_size}


@router.get("/anonymous-page-views", response_model=dict, summary="匿名访问记录")
async def anonymous_page_views(
    db: AsyncSession = Depends(get_db),
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=100),
    ip: str = Query("", description="IP 模糊搜索"),
    # location: str = Query("", description="所在地模糊搜索"),  # 已注释
    start_date: str = Query("", description="起始日期 YYYY-MM-DD"),
    end_date: str = Query("", description="截止日期 YYYY-MM-DD"),
):
    from datetime import datetime, timedelta

    # 默认最近 7 天（匿名日志保留期短）
    _now = datetime.now(UTC)
    _default_start = (_now - timedelta(days=7)).strftime("%Y-%m-%d")
    _default_end = _now.strftime("%Y-%m-%d")
    sd_str = start_date or _default_start
    ed_str = end_date or _default_end

    q = select(AnonymousPageView)
    count_q = select(func.count(AnonymousPageView.id))

    if ip:
        q = q.where(AnonymousPageView.ip_address.ilike(f"%{ip}%"))
        count_q = count_q.where(AnonymousPageView.ip_address.ilike(f"%{ip}%"))
    try:
        sd = datetime.strptime(sd_str, "%Y-%m-%d")
        q = q.where(AnonymousPageView.created_at >= sd)
        count_q = count_q.where(AnonymousPageView.created_at >= sd)
    except ValueError:
        pass
    try:
        ed = datetime.strptime(ed_str, "%Y-%m-%d") + timedelta(days=1)
        q = q.where(AnonymousPageView.created_at < ed)
        count_q = count_q.where(AnonymousPageView.created_at < ed)
    except ValueError:
        pass

    total = (await db.execute(count_q)).scalar() or 0
    offset = (page - 1) * page_size
    result = await db.execute(
        q.order_by(AnonymousPageView.created_at.desc())
        .offset(offset)
        .limit(page_size)
    )
    rows = result.scalars().all()

    # 并行查询地理位置
    ips = {r.ip_address for r in rows if r.ip_address}
    geo_map: dict[str, dict | None] = {}
    if ips:
        import asyncio
        tasks = {ip: lookup_ip(ip) for ip in ips}
        results = await asyncio.gather(*tasks.values())
        geo_map = dict(zip(tasks.keys(), results))

    items = []
    for r in rows:
        loc = geo_map.get(r.ip_address) if r.ip_address else None
        # if location and loc:  # 已注释
        #     loc_str = f"{loc.get('city','')} {loc.get('regionName','')} {loc.get('country','')}".lower()
        #     if location.lower() not in loc_str:
        #         continue
        items.append({
            "id": str(r.id),
            "anonymous_id": r.anonymous_id,
            "ip_address": r.ip_address,
            # "location": loc,  # 已注释
            "path": r.path,
            "page_title": r.page_title,
            "duration_seconds": r.duration_seconds,
            "user_agent": r.user_agent,
            "created_at": r.created_at.isoformat() if r.created_at else None,
        })

    # filtered_total = total if not location else len(items) + max(0, total - offset - len(rows))  # 已注释
    return {"items": items, "total": total, "page": page, "page_size": page_size}
