"""GlobalJournal 去重合并

从 global_journals 中检测并合并重复的期刊记录：
1. 同 ISSN 的多条记录 → 合并为一条
2. 无 ISSN 的模糊相似记录（trigram） → 合并
3. 不同 ISSN 但名称高度相似 → 合并（issn → eissn）

安全：UserJournalSubscription.journal_id 有 FK ON DELETE CASCADE，
因此先 UPDATE 再 DELETE。

用法：
  python scripts/dedup_global_journals.py            # 正式执行
  python scripts/dedup_global_journals.py --dry-run  # 只预览不变更
"""

import os
import argparse
import sys
import uuid as _uuid
from collections import Counter, defaultdict
from datetime import datetime

from sqlalchemy import create_engine, text, func
from sqlalchemy.orm import Session

DB_URL = os.environ.get("SEED_DB_URL") or "postgresql://scilit:scilit_dev@localhost:5432/scilit"
engine = create_engine(DB_URL, pool_pre_ping=True)


def _log(msg: str):
    print(f"  {msg}")


def collect_issn_duplicates(db: Session) -> list[dict]:
    """ISSN 重复检测：同 ISSN 多 ID → 合并计划"""
    rows = db.execute(
        text("""
            SELECT j.id, j.name, j.issn, j.created_at,
                   (SELECT COUNT(*) FROM user_journal_subscriptions WHERE journal_id = j.id) AS sub_count
            FROM global_journals j
            WHERE j.issn IS NOT NULL AND j.issn != ''
            ORDER BY j.issn, j.created_at
        """)
    ).all()

    groups: dict[str, list[dict]] = defaultdict(list)
    for r in rows:
        groups[r.issn].append({
            "id": r.id, "name": r.name, "issn": r.issn,
            "created_at": r.created_at, "sub_count": r.sub_count,
        })

    plans = []
    for issn, members in groups.items():
        if len(members) < 2:
            continue
        # winner = 关注数最多；其次最早创建
        winner = max(members, key=lambda m: (m["sub_count"], -(m["created_at"].timestamp() if m["created_at"] else 0)))
        mergees = [m for m in members if m["id"] != winner["id"]]
        # 统计该 ISSN 下最常见的名称
        name_counter: Counter = Counter()
        for m in members:
            name_counter[m["name"]] += 1
        canonical_name = name_counter.most_common(1)[0][0]
        plans.append({
            "type": "same_issn",
            "key": issn,
            "winner_id": winner["id"],
            "winner_name": winner["name"],
            "canonical_name": canonical_name,
            "merge_ids": [m["id"] for m in mergees],
            "total_subs": sum(m["sub_count"] for m in mergees),
        })
    return plans


def collect_fuzzy_no_issn_duplicates(db: Session, threshold: float = 0.8) -> list[dict]:
    """无 ISSN 模糊重复检测 — 用 pg_trgm similarity"""
    try:
        db.execute(text("SELECT similarity('test', 'test')"))
    except Exception:
        _log("⚠️  pg_trgm 扩展未安装，跳过无 ISSN 模糊匹配")
        return []

    rows = db.execute(
        text("""
            SELECT j.id, j.name, j.created_at,
                   (SELECT COUNT(*) FROM user_journal_subscriptions WHERE journal_id = j.id) AS sub_count
            FROM global_journals j
            WHERE (j.issn IS NULL OR j.issn = '')
            ORDER BY j.name
        """)
    ).all()

    records = [{"id": r.id, "name": r.name, "created_at": r.created_at, "sub_count": r.sub_count} for r in rows]

    # 全连接已访问记录
    visited = set()
    plans = []
    for i, a in enumerate(records):
        if a["id"] in visited:
            continue
        cluster = [a]
        visited.add(a["id"])
        for j, b in enumerate(records):
            if b["id"] in visited:
                continue
            sim = db.execute(
                text("SELECT similarity(:a, :b)"),
                {"a": a["name"], "b": b["name"]},
            ).scalar() or 0.0
            if sim >= threshold:
                cluster.append(b)
                visited.add(b["id"])

        if len(cluster) < 2:
            continue

        winner = max(cluster, key=lambda m: (m["sub_count"], -(m["created_at"].timestamp() if m["created_at"] else 0)))
        mergees = [m for m in cluster if m["id"] != winner["id"]]
        # 统计该簇最常见的名称
        name_counter: Counter = Counter()
        for m in cluster:
            name_counter[m["name"]] += 1
        canonical_name = name_counter.most_common(1)[0][0]
        plans.append({
            "type": "fuzzy_no_issn",
            "key": f"fuzzy:{a['name'][:40]}",
            "winner_id": winner["id"],
            "winner_name": winner["name"],
            "canonical_name": canonical_name,
            "cluster_names": [m["name"] for m in cluster],
            "merge_ids": [m["id"] for m in mergees],
            "total_subs": sum(m["sub_count"] for m in mergees),
        })
    return plans


def collect_cross_issn_duplicates(db: Session, threshold: float = 0.9) -> list[dict]:
    """跨 ISSN 名称相似检测 — 不同 ISSN 但名称高度相似"""
    try:
        db.execute(text("SELECT similarity('test', 'test')"))
    except Exception:
        return []

    rows = db.execute(
        text("""
            SELECT id, name, issn, eissn, created_at,
                   (SELECT COUNT(*) FROM user_journal_subscriptions WHERE journal_id = id) AS sub_count
            FROM global_journals
            WHERE issn IS NOT NULL AND issn != ''
            ORDER BY name
        """)
    ).all()

    records = [{"id": r.id, "name": r.name, "issn": r.issn, "eissn": r.eissn,
                 "created_at": r.created_at, "sub_count": r.sub_count} for r in rows]

    visited = set()
    plans = []
    for i, a in enumerate(records):
        if a["id"] in visited:
            continue
        cluster = [a]
        visited.add(a["id"])
        for j, b in enumerate(records):
            if b["id"] in visited:
                continue
            sim = db.execute(
                text("SELECT similarity(:a, :b)"),
                {"a": a["name"], "b": b["name"]},
            ).scalar() or 0.0
            if sim >= threshold and a["issn"] != b["issn"]:
                cluster.append(b)
                visited.add(b["id"])

        if len(cluster) < 2:
            continue

        winner = max(cluster, key=lambda m: (m["sub_count"],))
        mergees = [m for m in cluster if m["id"] != winner["id"]]

        # 收集被合并者的 issn → 放到 winner 的 eissn
        extra_issns = [m["issn"] for m in mergees if m["issn"] and m["issn"] != (winner.get("eissn") or "")]
        name_counter: Counter = Counter()
        for m in cluster:
            name_counter[m["name"]] += 1
        canonical_name = name_counter.most_common(1)[0][0]

        plans.append({
            "type": "cross_issn",
            "key": f"cross:{a['name'][:40]}",
            "winner_id": winner["id"],
            "winner_name": winner["name"],
            "canonical_name": canonical_name,
            "cluster_issns": [m["issn"] for m in cluster],
            "extra_issns": extra_issns,
            "merge_ids": [m["id"] for m in mergees],
            "total_subs": sum(m["sub_count"] for m in mergees),
        })
    return plans


def collect_zero_article_records(db: Session) -> list[dict]:
    """收集 article_count = 0 且 tier = '4' 且无订阅的记录（可安全清理）"""
    rows = db.execute(
        text("""
            SELECT j.id, j.name, j.issn, j.article_count, j.tier,
                   (SELECT COUNT(*) FROM user_journal_subscriptions WHERE journal_id = j.id) AS sub_count
            FROM global_journals j
            WHERE j.article_count = 0 AND j.tier = '4'
            ORDER BY sub_count, j.name
        """)
    ).all()

    candidates = []
    for r in rows:
        if r.sub_count > 0:
            continue
        candidates.append({
            "id": r.id, "name": r.name, "issn": r.issn,
        })
    return candidates


def recalculate_article_counts(db: Session):
    """从 global_literature 重新计算每个期刊的文章数"""
    _log("重新计算 journal article_count ...")
    db.execute(
        text("""
            UPDATE global_journals j
            SET article_count = COALESCE((
                SELECT COUNT(*) FROM global_literature
                WHERE journal_issn = j.issn
            ), 0),
            updated_at = NOW()
        """)
    )
    db.commit()


def print_summary(all_plans: list[dict], zero_articles: list[dict]):
    """打印合并计划预览"""
    total_merge = sum(len(p["merge_ids"]) for p in all_plans)
    total_subs_moved = sum(p["total_subs"] for p in all_plans)
    print(f"\n{'='*60}")
    print(f"去重计划预览")
    print(f"{'='*60}")
    print(f"  合并操作总计: {len(all_plans)}")
    print(f"  涉及冗余记录: {total_merge}")
    print(f"  需迁移的订阅: {total_subs_moved}")
    print(f"  零文章可清理: {len(zero_articles)}")
    print()

    by_type = Counter(p["type"] for p in all_plans)
    for t, c in by_type.most_common():
        print(f"  [{t}] {c} 组")

    if all_plans:
        print(f"\n  --- 详细 ---")
        for p in all_plans[:20]:
            print(f"  [{p['type']:15}] {p['key'][:50]}")
            print(f"         保留: {p['winner_name'][:60]} ({p['winner_id']})")
            print(f"         规范名: {p['canonical_name'][:60]}")
            print(f"         合并: {len(p['merge_ids'])} 条, 迁移 {p['total_subs']} 个订阅")
        if len(all_plans) > 20:
            print(f"  ... 还有 {len(all_plans) - 20} 组未显示")

    if zero_articles:
        print(f"\n  可安全清理的零文章记录（tier=4, 无订阅）:")
        for r in zero_articles[:10]:
            print(f"    {r['name'][:60]} (issn={r['issn']})")
        if len(zero_articles) > 10:
            print(f"    ... 还有 {len(zero_articles) - 10} 条")


def execute_merge(db: Session, plans: list[dict]):
    """执行合并：UPDATE 订阅 → UPDATE 规范名/issn → DELETE 冗余"""
    total = len(plans)
    for idx, p in enumerate(plans, 1):
        _log(f"[{idx}/{total}] 合并 [{p['type']}] {p['key'][:50]}")

        winner_id = p["winner_id"]
        merge_ids = p["merge_ids"]

        # 1. 迁移订阅
        if p["total_subs"] > 0:
            db.execute(
                text("""
                    UPDATE user_journal_subscriptions
                    SET journal_id = :winner
                    WHERE journal_id = ANY(:merge_ids)
                      AND NOT EXISTS (
                          SELECT 1 FROM user_journal_subscriptions
                          WHERE journal_id = :winner AND user_id = user_journal_subscriptions.user_id
                      )
                """),
                {"winner": winner_id, "merge_ids": merge_ids},
            )
            # 删除剩余重复订阅（同 user 已指向 winner 的）
            db.execute(
                text("""
                    DELETE FROM user_journal_subscriptions
                    WHERE journal_id = ANY(:merge_ids)
                """),
                {"merge_ids": merge_ids},
            )

        # 2. 更新规范名称
        db.execute(
            text("UPDATE global_journals SET name = :name, updated_at = NOW() WHERE id = :id"),
            {"name": p["canonical_name"], "id": winner_id},
        )

        # 3. 跨 ISSN 合并：将合并者的 issn 移到 winner 的 eissn
        if p["type"] == "cross_issn" and p.get("extra_issns"):
            existing_eissn = db.execute(
                text("SELECT eissn FROM global_journals WHERE id = :id"),
                {"id": winner_id},
            ).scalar()
            for extra_issn in p["extra_issns"]:
                if not existing_eissn:
                    db.execute(
                        text("UPDATE global_journals SET eissn = :eissn WHERE id = :id"),
                        {"eissn": extra_issn, "id": winner_id},
                    )
                    existing_eissn = extra_issn
                # 更新 global_literature.journal_issn 中的引用指向 winner
                db.execute(
                    text("""
                        UPDATE global_literature
                        SET journal_issn = :winner_issn
                        WHERE journal_issn = :old_issn
                    """),
                    {"winner_issn": p["cluster_issns"][0] if p["cluster_issns"] else None,
                     "old_issn": extra_issn},
                )

        # 4. 同 ISSN 合并：更新 global_literature 中的 journal 名称为规范名
        if p["type"] == "same_issn":
            for merge_id in merge_ids:
                merge_issn = db.execute(
                    text("SELECT issn FROM global_journals WHERE id = :id"),
                    {"id": merge_id},
                ).scalar()
                if merge_issn:
                    db.execute(
                        text("""
                            UPDATE global_literature
                            SET journal = :canonical_name
                            WHERE journal_issn = :issn
                        """),
                        {"canonical_name": p["canonical_name"], "issn": merge_issn},
                    )

        # 5. 删除冗余记录（FK CASCADE 已通过订阅迁移规避）
        db.execute(
            text("DELETE FROM global_journals WHERE id = ANY(:ids)"),
            {"ids": merge_ids},
        )

    db.commit()


def cleanup_zero_articles(db: Session, candidates: list[dict]):
    """清理零文章记录"""
    ids = [c["id"] for c in candidates]
    if not ids:
        return
    _log(f"清理 {len(ids)} 条零文章记录 ...")
    db.execute(
        text("DELETE FROM global_journals WHERE id = ANY(:ids)"),
        {"ids": ids},
    )
    db.commit()


def main():
    parser = argparse.ArgumentParser(description="GlobalJournal 去重合并")
    parser.add_argument("--dry-run", action="store_true", help="只预览不变更")
    parser.add_argument("--fuzzy-threshold", type=float, default=0.8,
                        help="无 ISSN 模糊匹配阈值 (默认 0.8)")
    parser.add_argument("--cross-threshold", type=float, default=0.9,
                        help="跨 ISSN 名称相似匹配阈值 (默认 0.9)")
    args = parser.parse_args()

    dry_run = args.dry_run

    print(f"连接到数据库 ...")
    with Session(engine) as db:
        # 去重前统计
        before_count = db.execute(text("SELECT COUNT(*) FROM global_journals")).scalar()
        before_subs = db.execute(text("SELECT COUNT(*) FROM user_journal_subscriptions")).scalar()

        print(f"\n{'='*60}")
        print(f"去重前统计")
        print(f"{'='*60}")
        print(f"  global_journals: {before_count} 条")
        print(f"  user_journal_subscriptions: {before_subs} 条")

        # Step 1: 收集重复
        print(f"\n{'='*60}")
        print(f"阶段 1/4: ISSN 重复检测")
        print(f"{'='*60}")
        issn_plans = collect_issn_duplicates(db)
        _log(f"找到 {len(issn_plans)} 组 ISSN 重复")

        print(f"\n{'='*60}")
        print(f"阶段 2/4: 无 ISSN 模糊重复检测 (threshold={args.fuzzy_threshold})")
        print(f"{'='*60}")
        fuzzy_plans = collect_fuzzy_no_issn_duplicates(db, threshold=args.fuzzy_threshold)
        _log(f"找到 {len(fuzzy_plans)} 组无 ISSN 模糊重复")

        print(f"\n{'='*60}")
        print(f"阶段 3/4: 跨 ISSN 名称相似检测 (threshold={args.cross_threshold})")
        print(f"{'='*60}")
        cross_plans = collect_cross_issn_duplicates(db, threshold=args.cross_threshold)
        _log(f"找到 {len(cross_plans)} 组跨 ISSN 名称相似")

        # Step 2: 零文章记录
        print(f"\n{'='*60}")
        print(f"阶段 4/4: 零文章记录检测")
        print(f"{'='*60}")
        zero_articles = collect_zero_article_records(db)
        _log(f"找到 {len(zero_articles)} 条零文章记录")

        # 合并所有计划
        all_plans = issn_plans + fuzzy_plans + cross_plans

        # 预览
        print_summary(all_plans, zero_articles)

        if dry_run:
            print(f"\n[Dry Run] 未执行任何变更。加 --dry-run 去掉后正式执行。")
            final_count = before_count - sum(len(p["merge_ids"]) for p in all_plans) - len(zero_articles)
            print(f"预计去重后 global_journals: {before_count} → {final_count} (减少 {before_count - final_count})")
            return

        # 正式执行
        print(f"\n{'='*60}")
        print(f"执行合并 ...")
        print(f"{'='*60}")

        execute_merge(db, all_plans)
        recalculate_article_counts(db)
        cleanup_zero_articles(db, zero_articles)

        # 去重后统计
        after_count = db.execute(text("SELECT COUNT(*) FROM global_journals")).scalar()
        after_subs = db.execute(text("SELECT COUNT(*) FROM user_journal_subscriptions")).scalar()

        print(f"\n{'='*60}")
        print(f"去重完成")
        print(f"{'='*60}")
        print(f"  global_journals: {before_count} → {after_count} (减少 {before_count - after_count})")
        print(f"  user_journal_subscriptions: {before_subs} → {after_subs}")
        print(f"  注意：subscriptions 减少 = 原指向重复记录的重复订阅被清理")


if __name__ == "__main__":
    main()
