"""
Наукометрика reviewer4 v1.1
Измерение объективных показателей: citations, h-index, FWCI-proxy

API: Semantic Scholar (бесплатно, 1 rps без ключа)
Альтернатива FWCI: Scopus API (платный) — в этой версии используем proxy
Дедлайн: четверг 2026-02-27 (Даниил → Альберт, AI4S)

Usage:
  python naукометрика_reviewer4.py               # тест на предустановленных авторах
  python naукометрика_reviewer4.py --names "Mikhail Gelfand" "Yann LeCun"
  python naукометрика_reviewer4.py --ids 1688882 1741101  # Semantic Scholar IDs
  python naукометрика_reviewer4.py --output report.md     # кастомный выход

Outputs:
  data/naukometrika_results.json    — полные данные
  data/naukometrika_results.csv     — таблица для Excel
  data/naukometrika_report.md       — Markdown отчёт
"""

import argparse
import csv
import json
import sys
import time
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Optional

import requests

BASE_URL = "https://api.semanticscholar.org/graph/v1"
CURRENT_YEAR = 2026
RECENT_YEARS = 5  # статьи за 2021-2026


@dataclass
class AuthorMetrics:
    name: str
    query: str               # исходный запрос
    semantic_scholar_id: Optional[str]
    h_index: Optional[int]
    citation_count: Optional[int]
    paper_count: Optional[int]
    papers_last_5y: int
    citations_last_5y: int
    citations_per_paper: Optional[float]   # proxy FWCI (не нормировано по области)
    fwci_note: str
    top_paper_title: Optional[str]
    top_paper_citations: Optional[int]
    top_paper_year: Optional[int]
    ss_url: Optional[str]


def _get(url: str, params: dict, retries: int = 3) -> dict:
    """GET с ретраями на rate limit."""
    for attempt in range(retries):
        try:
            resp = requests.get(url, params=params, timeout=15)
            if resp.status_code == 429:
                wait = 10 * (attempt + 1)
                print(f"  Rate limit — ждём {wait}с...", file=sys.stderr)
                time.sleep(wait)
                continue
            resp.raise_for_status()
            return resp.json()
        except requests.RequestException as e:
            if attempt == retries - 1:
                raise
            time.sleep(2)
    return {}


def search_author(name: str) -> Optional[dict]:
    """Найти автора по имени. Возвращает кандидата с наибольшим числом цитирований."""
    data = _get(
        f"{BASE_URL}/author/search",
        {"query": name, "fields": "authorId,name,hIndex,citationCount,paperCount", "limit": 5}
    )
    candidates = data.get("data", [])
    if not candidates:
        return None
    # Берём кандидата с max цитирований (не просто первого)
    return max(candidates, key=lambda a: a.get("citationCount", 0))


def get_author_by_id(author_id: str) -> Optional[dict]:
    """Получить автора напрямую по SS ID."""
    data = _get(
        f"{BASE_URL}/author/{author_id}",
        {"fields": "authorId,name,hIndex,citationCount,paperCount"}
    )
    return data if data.get("authorId") else None


def get_author_papers(author_id: str) -> list[dict]:
    """Получить все статьи автора (пагинация по 500)."""
    papers = []
    offset = 0
    limit = 500
    while True:
        data = _get(
            f"{BASE_URL}/author/{author_id}/papers",
            {"fields": "title,year,citationCount,venue,publicationTypes", "limit": limit, "offset": offset}
        )
        batch = data.get("data", [])
        papers.extend(batch)
        if len(batch) < limit:
            break
        offset += limit
        time.sleep(0.3)
    return papers


def compute_metrics(query: str, ss_id: Optional[str] = None) -> AuthorMetrics:
    """Вычислить метрики для одного автора."""
    # 1. Найти автора
    if ss_id:
        author = get_author_by_id(ss_id)
    else:
        author = search_author(query)

    if not author:
        return AuthorMetrics(
            name=query, query=query, semantic_scholar_id=None,
            h_index=None, citation_count=None, paper_count=None,
            papers_last_5y=0, citations_last_5y=0,
            citations_per_paper=None, fwci_note="Автор не найден",
            top_paper_title=None, top_paper_citations=None,
            top_paper_year=None, ss_url=None
        )

    author_id = author["authorId"]
    time.sleep(0.5)  # 1 rps без ключа

    # 2. Статьи
    papers = get_author_papers(author_id)

    # 3. Последние 5 лет
    recent_papers = [p for p in papers if p.get("year") and p["year"] >= CURRENT_YEAR - RECENT_YEARS]
    papers_last_5y = len(recent_papers)
    citations_last_5y = sum(p.get("citationCount", 0) for p in recent_papers)

    # 4. Топ статья
    top_paper = max(papers, key=lambda p: p.get("citationCount", 0), default=None)

    # 5. Citations per paper (proxy FWCI)
    paper_count = author.get("paperCount", 0)
    citation_count = author.get("citationCount", 0)
    cpp = round(citation_count / paper_count, 1) if paper_count > 0 else None

    return AuthorMetrics(
        name=author.get("name", query),
        query=query,
        semantic_scholar_id=author_id,
        h_index=author.get("hIndex"),
        citation_count=citation_count,
        paper_count=paper_count,
        papers_last_5y=papers_last_5y,
        citations_last_5y=citations_last_5y,
        citations_per_paper=cpp,
        fwci_note=f"Proxy (cit/papers). Реальный FWCI — Scopus.",
        top_paper_title=top_paper.get("title") if top_paper else None,
        top_paper_citations=top_paper.get("citationCount") if top_paper else None,
        top_paper_year=top_paper.get("year") if top_paper else None,
        ss_url=f"https://www.semanticscholar.org/author/{author_id}" if author_id else None
    )


def score_applicant(m: AuthorMetrics) -> dict:
    """
    Рейтинг заявителя (0-100).
    Используется как сигнал reviewer4.
    """
    score = 0
    flags = []
    h = m.h_index or 0
    cit = m.citation_count or 0
    papers5 = m.papers_last_5y or 0
    cpp = m.citations_per_paper or 0

    # h-индекс (35%)
    if h >= 20:    score += 35; flags.append("h≥20 🏆")
    elif h >= 10:  score += 25; flags.append("h≥10 ✅")
    elif h >= 5:   score += 15
    elif h >= 1:   score += 5

    # Цитирования всего (25%)
    if cit >= 1000:   score += 25; flags.append("cit≥1K 🏆")
    elif cit >= 200:  score += 20
    elif cit >= 50:   score += 10
    elif cit >= 10:   score += 5

    # Активность последние 5 лет (25%)
    if papers5 >= 10:  score += 25; flags.append("active 🔥")
    elif papers5 >= 5: score += 20
    elif papers5 >= 2: score += 10
    elif papers5 >= 1: score += 5

    # Citations per paper (15%)
    if cpp >= 5:   score += 15; flags.append("impact 📈")
    elif cpp >= 2: score += 10
    elif cpp >= 1: score += 7

    tier = ("A" if score >= 70 else "B" if score >= 40 else "C")

    return {
        "query": m.query,
        "name": m.name,
        "score": min(score, 100),
        "tier": tier,
        "flags": flags,
        "h_index": h,
        "citation_count": cit,
        "paper_count": m.paper_count,
        "papers_last_5y": papers5,
        "citations_last_5y": m.citations_last_5y,
        "citations_per_paper": cpp,
        "ss_url": m.ss_url,
        "top_paper": m.top_paper_title,
        "top_paper_citations": m.top_paper_citations,
    }


def evaluate_batch(names: list[str], ids: list[str] = None) -> tuple[list[dict], list[dict]]:
    """Оценить список авторов. Возвращает (raw_metrics, scored)."""
    raw_metrics = []
    ids = ids or []
    id_map = dict(zip(names[:len(ids)], ids))  # name → ss_id if known

    for name in names:
        ss_id = id_map.get(name)
        label = name + (f" [{ss_id}]" if ss_id else "")
        print(f"  Processing: {label}...")
        try:
            m = compute_metrics(name, ss_id)
            raw_metrics.append(asdict(m))
            print(f"    h={m.h_index}, cit={m.citation_count}, papers5y={m.papers_last_5y}")
        except Exception as e:
            print(f"    ERROR: {e}")
            raw_metrics.append({"query": name, "name": name, "error": str(e)})

    scores = [score_applicant(AuthorMetrics(**{k: v for k, v in r.items() if k in AuthorMetrics.__dataclass_fields__}))
              for r in raw_metrics if "error" not in r]
    scores.sort(key=lambda x: x["score"], reverse=True)
    return raw_metrics, scores


def save_json(data: dict, path: Path):
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
    print(f"  JSON: {path}")


def save_csv(scores: list[dict], path: Path):
    if not scores:
        return
    fields = ["score", "tier", "name", "h_index", "citation_count", "paper_count",
              "papers_last_5y", "citations_last_5y", "citations_per_paper", "flags", "ss_url"]
    with open(path, "w", newline="", encoding="utf-8-sig") as f:
        w = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
        w.writeheader()
        for s in scores:
            row = {**s, "flags": "; ".join(s.get("flags", []))}
            w.writerow(row)
    print(f"  CSV: {path}")


def save_markdown(scores: list[dict], raw: list[dict], path: Path):
    lines = [
        "# Наукометрика — Рейтинг авторов",
        f"*Дата: 2026-02-25 | API: Semantic Scholar | FWCI = proxy (cit/papers)*",
        "",
        "## Рейтинг",
        "",
        "| # | Имя | Тир | Score | h | Цит. всего | Статей 5л | Цит. 5л | Cit/paper |",
        "|---|-----|-----|-------|---|-----------|-----------|---------|-----------|",
    ]
    for i, s in enumerate(scores, 1):
        flags = " ".join(s.get("flags", []))
        lines.append(
            f"| {i} | [{s['name']}]({s['ss_url'] or '#'}) | **{s['tier']}** | {s['score']}/100 "
            f"| {s['h_index']} | {s['citation_count']:,} | {s['papers_last_5y']} "
            f"| {s['citations_last_5y']:,} | {s['citations_per_paper']} |"
        )

    lines += [
        "",
        "## Топ статьи",
        "",
    ]
    for s in scores[:5]:
        r = next((x for x in raw if x.get("name") == s["name"]), {})
        tp = r.get("top_paper_title", "—")
        tc = r.get("top_paper_citations", "—")
        ty = r.get("top_paper_year", "")
        lines.append(f"- **{s['name']}**: *{tp}* ({ty}, {tc} цитирований)")

    lines += [
        "",
        "## Методология",
        "",
        "- **h-index**: из Semantic Scholar (агрегат по всем источникам)",
        "- **citations**: суммарные цитирования автора",
        "- **FWCI proxy**: citations / papers (не нормировано по области — нужен Scopus API для настоящего FWCI)",
        "- **Тир**: A (≥70 баллов), B (40-69), C (<40)",
        "- **Score**: h(35%) + цит.(25%) + активность 5л(25%) + impact(15%)",
        "",
        f"*Сгенерировано Феанором, reviewer4 v1.1*",
    ]

    path.write_text("\n".join(lines), encoding="utf-8")
    print(f"  Markdown: {path}")


# ── Предустановленные тестовые авторы (российские ML/AI исследователи) ─────────

TEST_AUTHORS = [
    # Международные звёзды ML (для калибровки)
    "Yann LeCun",
    "Andrej Karpathy",
    # Российские учёные в ML/AI (для Научной премии Сбера)
    "Mikhail Gelfand",
    "Dmitry Vetrov",
    "Victor Lempitsky",
    "Evgenia Selskaya",
    # [REMOVED: incorrect научрук reference]
    "Maxim Panov",
    "Ivan Oseledets",
    "Andrei Akhrem",
]

# Известные Semantic Scholar ID (для исключения неоднозначности):
# Можно добавить SS ID через --ids аргумент
KNOWN_IDS: dict[str, str] = {
    "Yann LeCun": "1688882",
    "Ivan Oseledets": "1738205",
    "Dmitry Vetrov": "2492721",
}


def main():
    parser = argparse.ArgumentParser(description="Наукометрика reviewer4")
    parser.add_argument("--names", nargs="+", help="Имена авторов")
    parser.add_argument("--ids", nargs="+", help="Semantic Scholar IDs (по порядку имён)")
    parser.add_argument("--output-dir", default="data", help="Директория для результатов")
    args = parser.parse_args()

    names = args.names or TEST_AUTHORS
    ss_ids = args.ids or [KNOWN_IDS.get(n) for n in names]

    out_dir = Path(args.output_dir)
    out_dir.mkdir(parents=True, exist_ok=True)

    print(f"=== Наукометрика reviewer4 v1.1 ===")
    print(f"Авторов: {len(names)}")
    print()

    raw, scores = evaluate_batch(names, ss_ids)

    print("\n=== Рейтинг ===")
    for s in scores:
        flags_str = "  " + " ".join(s["flags"]) if s["flags"] else ""
        print(
            f"  [{s['tier']}] {s['score']:3d}/100  {s['name']:<30s} "
            f"h={s['h_index']:<4} cit={s['citation_count']:<8} "
            f"5y={s['papers_last_5y']:<4}{flags_str}"
        )

    print("\n=== Сохранение ===")
    payload = {
        "version": "1.1",
        "date": "2026-02-25",
        "authors_count": len(names),
        "raw_metrics": raw,
        "ranking": scores,
        "note": "FWCI = proxy (cit/papers). Реальный FWCI — только через Scopus API."
    }
    save_json(payload, out_dir / "naukometrika_results.json")
    save_csv(scores, out_dir / "naukometrika_results.csv")
    save_markdown(scores, raw, out_dir / "naukometrika_report.md")
    print("\nГотово!")


if __name__ == "__main__":
    main()
