#!/usr/bin/env python3
"""回溯清理误判文献。

按 Phase 0 新规则重新扫描已入库的 153 万条文献，
删除不符合 C04 肿瘤学范围（有 MeSH）/文本评分不达标（无 MeSH）的记录。

用法:
    # 1. 先看统计
    python scripts/cleanup_false_positives.py --mode stats

    # 2. 看样例
    python scripts/cleanup_false_positives.py --mode dry-run --dry-limit 20

    # 3. 实际删除
    python scripts/cleanup_false_positives.py --mode delete

    # 限制扫描量（调试用）
    python scripts/cleanup_false_positives.py --mode stats --max 50000
"""

import argparse
import sys
import time
from pathlib import Path

# ── 路径引导 ──────────────────────────────────────────────────────────────────
_SCRIPT_DIR = str(Path(__file__).parent)
_PROJECT_ROOT = str(Path(__file__).parent.parent)
if _PROJECT_ROOT not in sys.path:
    sys.path.insert(0, _PROJECT_ROOT)
if _SCRIPT_DIR not in sys.path:
    sys.path.insert(0, _SCRIPT_DIR)

from sqlalchemy import select, func, delete as sa_delete

from app.db import async_session
from app.models.literature import GlobalLiterature
from app.services.mesh_filter import get_mesh_filter


BATCH_QUERY = 1000   # 每次查询条数
BATCH_DELETE = 500   # 每次删除条数


async def scan_records(
    mode: str, batch_size: int = BATCH_QUERY, max_records: int = 0, dry_limit: int = 20
) -> dict:
    """扫描全表，统计/预览/删除不符合新规则的记录。

    返回:
        {'scanned': N, 'to_delete': N, 'deleted': N, 'samples': [...], 'time': seconds}
    """
    mf = get_mesh_filter()
    stats = {
        "scanned": 0,
        "to_delete": 0,
        "deleted": 0,
        "samples": [],
    }

    # 获取总量（用于进度显示）
    async with async_session() as db:
        total = await db.scalar(select(func.count(GlobalLiterature.id)))

    ids_to_delete: list[str] = []
    last_id = None  # keyset pagination
    start_ts = time.time()

    while True:
        async with async_session() as db:
            # keyset 分页: 按 id 排序，每次取 batch_size 条
            stmt = select(
                GlobalLiterature.id,
                GlobalLiterature.pmid,
                GlobalLiterature.title,
                GlobalLiterature.abstract,
                GlobalLiterature.mesh_headings,
            ).order_by(GlobalLiterature.id)

            if last_id is not None:
                stmt = stmt.where(GlobalLiterature.id > last_id)

            stmt = stmt.limit(batch_size)
            rows = (await db.execute(stmt)).all()

        if not rows:
            break

        for row in rows:
            stats["scanned"] += 1
            last_id = row.id

            mesh_headings = row.mesh_headings or []
            should_delete = False
            reason = ""

            if mesh_headings:
                # Tier 1: 有 MeSH → 检查 C04 树号
                if not mf.is_oncology_by_mesh(mesh_headings):
                    should_delete = True
                    reason = f"has_mesh_but_no_c04"
            else:
                # Tier 2: 无 MeSH → 文本评分
                if not mf.is_oncology_by_text_score(row.title, row.abstract):
                    should_delete = True
                    reason = f"no_mesh_text_fail"

            if should_delete:
                stats["to_delete"] += 1
                ids_to_delete.append(str(row.id))
                if len(stats["samples"]) < dry_limit:
                    stats["samples"].append({
                        "pmid": row.pmid,
                        "reason": reason,
                        "title": (row.title or "")[:100],
                    })

        if max_records and stats["scanned"] >= max_records:
            break

        # 进度
        if stats["scanned"] % (batch_size * 10) == 0:
            pct = stats["scanned"] / total * 100 if total else 0
            elapsed = time.time() - start_ts
            print(
                f"  Scanned: {stats['scanned']:,}/{total:,} "
                f"({pct:.1f}%), found: {stats['to_delete']:,} to delete, "
                f"{elapsed:.0f}s elapsed",
                flush=True,
            )

    # ── 删除阶段（仅 mode=delete） ───────────────────────────────────────────
    if mode == "delete" and ids_to_delete:
        print(f"\n  Deleting {len(ids_to_delete):,} records in batches of {BATCH_DELETE}...", flush=True)
        delete_start = time.time()

        for i in range(0, len(ids_to_delete), BATCH_DELETE):
            chunk = ids_to_delete[i : i + BATCH_DELETE]
            async with async_session() as db:
                stmt = sa_delete(GlobalLiterature).where(
                    GlobalLiterature.id.in_(chunk)
                )
                result = await db.execute(stmt)
                await db.commit()
                stats["deleted"] += result.rowcount

            if (i // BATCH_DELETE) % 20 == 0:
                print(
                    f"  Deleted: {stats['deleted']:,}/{len(ids_to_delete):,} "
                    f"({stats['deleted'] / len(ids_to_delete) * 100:.1f}%)",
                    flush=True,
                )

        print(
            f"  Delete complete: {stats['deleted']:,} records in "
            f"{time.time() - delete_start:.0f}s",
            flush=True,
        )

    stats["time"] = time.time() - start_ts
    return stats


async def main():
    parser = argparse.ArgumentParser(
        description="Back-clean false-positive records using Phase 0 filter rules"
    )
    parser.add_argument(
        "--mode",
        choices=["stats", "dry-run", "delete"],
        default="stats",
        help="stats=count only, dry-run=show samples, delete=actually delete",
    )
    parser.add_argument(
        "--batch",
        type=int,
        default=BATCH_QUERY,
        help=f"Query batch size (default {BATCH_QUERY})",
    )
    parser.add_argument(
        "--dry-limit",
        type=int,
        default=20,
        help="Max sample PMIDs to show in dry-run (default 20)",
    )
    parser.add_argument(
        "--max",
        type=int,
        default=0,
        help="Max records to scan (default 0 = all, for debugging)",
    )
    args = parser.parse_args()

    mode_label = {"stats": "STATS ONLY", "dry-run": "DRY RUN", "delete": "DELETE"}[
        args.mode
    ]
    print(f"=== Cleanup Mode: {mode_label} ===")
    print(f"  Batch: {args.batch} per query")
    if args.max:
        print(f"  Max records: {args.max}")
    if args.mode == "stats":
        print(f"  No records will be deleted.")
    elif args.mode == "dry-run":
        print(f"  Showing up to {args.dry_limit} sample records. No deletions.")
    else:
        print(f"  WARNING: Records WILL be deleted (CASCADE to child tables).")

    # Warm up MeshFilter
    print("\nLoading MeshFilter (mtrees)...", end=" ", flush=True)
    get_mesh_filter()
    print("OK")

    if args.mode == "delete":
        print("\n" + "!" * 60)
        print("  Are you sure you want to DELETE records?")
        print("  This will CASCADE to: user_feed, user_literature, user_notes,")
        print("  user_feedback, global_literature_tags, review_literatures, etc.")
        print("!" * 60)
        confirm = input("\nType 'yes' to confirm: ")
        if confirm.strip().lower() != "yes":
            print("Aborted.")
            return

    print(f"\nScanning records...")
    result = await scan_records(
        args.mode,
        batch_size=args.batch,
        max_records=args.max,
        dry_limit=args.dry_limit,
    )

    # ── Summary ───────────────────────────────────────────────────────────────
    print("\n" + "=" * 60)
    print("  CLEANUP SUMMARY")
    print("=" * 60)
    print(f"  Total scanned:  {result['scanned']:>10,}")
    print(f"  To delete:      {result['to_delete']:>10,}")
    if args.mode == "delete":
        print(f"  Actually deleted: {result['deleted']:>10,}")
    print(f"  Time:           {result['time']:>10.1f}s")

    if result["samples"]:
        print(f"\n  Sample records to be deleted (up to {args.dry_limit}):")
        print(f"  {'PMID':>10}  {'Reason':<24}  {'Title'}")
        print(f"  {'-'*10}  {'-'*24}  {'-'*50}")
        for s in result["samples"]:
            print(f"  {s['pmid']:>10}  {s['reason']:<24}  {s['title']}")

    if args.mode in ("stats", "dry-run") and result["to_delete"] > 0:
        print(
            f"\n  Run with --mode delete to actually remove "
            f"{result['to_delete']:,} records."
        )


if __name__ == "__main__":
    import asyncio

    asyncio.run(main())
