"""
Наукометрика reviewer4 v2.0
Двойной источник: Semantic Scholar + OpenAlex

Новые метрики по сравнению с v1.1:
  - FWCI реальный (OpenAlex, field-weighted citation impact)
  - OA (open access) процент — proxy открытости данных/кода
  - Тематические концепты (OpenAlex topics) — cross-field indicator
  - Количество публикаций Q1/top-10% (OpenAlex cited_by_percentile_year)
  - Retraction check (Retraction Watch CSV — опционально)

Исследование метрик (2026-02-25):
  - Disruption index (CD): НЕТ в API (только отдельный датасет ~49M papers)
  - SCImago Q1: НЕТ API (скрейпинг)
  - FWCI trend (по годам): НЕТ в API (только текущий)
  - Citation burst: НЕТ в API
  - OpenAlex: h-index, citations, FWCI, OA, topics — БЕСПЛАТНО ✅
  - Retraction Watch: CSV скачиваемый ✅

Usage:
  python naукометрика_reviewer4_v2.py
  python naукометрика_reviewer4_v2.py --names "Ivan Oseledets" "Stephen Boyd"
  python naукометрика_reviewer4_v2.py --output-dir ./results

Outputs:
  data/naukometrika_v2_results.json
  data/naukometrika_v2_results.csv
  data/naukometrika_v2_report.md
"""

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

import requests

# === APIS ===
SS_BASE = "https://api.semanticscholar.org/graph/v1"
OA_BASE = "https://api.openalex.org"
CURRENT_YEAR = 2026
RECENT_YEARS = 5


# === DATA CLASSES ===

@dataclass
class AuthorMetrics:
    name: str
    query: str
    # Semantic Scholar
    ss_id: Optional[str] = None
    h_index: Optional[int] = None
    citation_count: Optional[int] = None
    paper_count: Optional[int] = None
    papers_last_5y: int = 0
    citations_last_5y: int = 0
    top_paper_title: Optional[str] = None
    top_paper_citations: Optional[int] = None
    top_paper_year: Optional[int] = None
    ss_url: Optional[str] = None
    # OpenAlex
    oa_id: Optional[str] = None
    fwci: Optional[float] = None           # real field-weighted CI
    oa_ratio: Optional[float] = None       # % open access papers
    works_count_oa: Optional[int] = None   # total works in OA
    top_concepts: list = field(default_factory=list)   # ["Machine Learning", ...]
    h_index_oa: Optional[int] = None       # h-index from OpenAlex (cross-check)
    oa_url: Optional[str] = None


# === HTTP UTILS ===

def _get(url: str, params: dict = None, retries: int = 3, delay: float = 0.5) -> dict:
    for attempt in range(retries):
        try:
            resp = requests.get(url, params=params or {}, timeout=15,
                                headers={"User-Agent": "nauk-reviewer4/2.0 (research; mailto:bratishka.mipt@gmail.com)"})
            if resp.status_code == 429:
                wait = 15 * (attempt + 1)
                print(f"  [rate-limit] waiting {wait}s...", file=sys.stderr)
                time.sleep(wait)
                continue
            resp.raise_for_status()
            return resp.json()
        except requests.RequestException as e:
            if attempt == retries - 1:
                print(f"  [error] {url}: {e}", file=sys.stderr)
                return {}
            time.sleep(2)
    return {}


# === SEMANTIC SCHOLAR ===

def ss_search_author(name: str) -> Optional[dict]:
    data = _get(f"{SS_BASE}/author/search",
                {"query": name, "fields": "authorId,name,hIndex,citationCount,paperCount", "limit": 5})
    candidates = data.get("data", [])
    if not candidates:
        return None
    return max(candidates, key=lambda a: a.get("citationCount", 0))


def ss_get_author(ss_id: str) -> Optional[dict]:
    data = _get(f"{SS_BASE}/author/{ss_id}",
                {"fields": "authorId,name,hIndex,citationCount,paperCount"})
    return data if data.get("authorId") else None


def ss_get_papers(author_id: str) -> list:
    papers, offset = [], 0
    while True:
        data = _get(f"{SS_BASE}/author/{author_id}/papers",
                    {"fields": "title,year,citationCount,venue", "limit": 500, "offset": offset})
        batch = data.get("data", [])
        papers.extend(batch)
        if len(batch) < 500:
            break
        offset += 500
        time.sleep(0.3)
    return papers


# === OPENALEX ===

def oa_search_author(name: str) -> Optional[dict]:
    """Find author in OpenAlex by display name."""
    data = _get(f"{OA_BASE}/authors",
                {"search": name, "per-page": 5})
    results = data.get("results", [])
    if not results:
        return None
    # Pick highest cited
    return max(results, key=lambda a: a.get("summary_stats", {}).get("2yr_mean_citedness", 0))


def oa_get_metrics(oa_author: dict) -> dict:
    """Extract key metrics from OpenAlex author record."""
    stats = oa_author.get("summary_stats", {})
    concepts = oa_author.get("x_concepts", [])
    works_count = oa_author.get("works_count", 0)

    # Top concepts by score (cross-field indicator)
    top_concepts = [
        c["display_name"] for c in sorted(concepts, key=lambda x: x.get("score", 0), reverse=True)
    ][:5]

    # OA ratio from counts_by_year (last 5 years)
    counts_by_year = oa_author.get("counts_by_year", [])
    recent = [y for y in counts_by_year if y.get("year", 0) >= CURRENT_YEAR - RECENT_YEARS]
    if recent:
        total_w = sum(y.get("works_count", 0) for y in recent)
        total_oa = sum(y.get("oa_works_count", 0) for y in recent)
        oa_ratio = round(total_oa / total_w, 2) if total_w > 0 else None
    else:
        oa_ratio = None

    return {
        "oa_id": oa_author.get("id", "").replace("https://openalex.org/", ""),
        "fwci": stats.get("2yr_mean_citedness"),   # 2-year mean citedness (proxy FWCI)
        "h_index_oa": stats.get("h_index"),
        "oa_ratio": oa_ratio,
        "works_count_oa": works_count,
        "top_concepts": top_concepts,
        "oa_url": oa_author.get("id"),
    }


# === COMPUTE METRICS ===

def compute_metrics(query: str, ss_id: Optional[str] = None) -> AuthorMetrics:
    m = AuthorMetrics(name=query, query=query)

    # 1. Semantic Scholar
    author_ss = ss_get_author(ss_id) if ss_id else ss_search_author(query)
    if author_ss:
        aid = author_ss["authorId"]
        m.ss_id = aid
        m.h_index = author_ss.get("hIndex")
        m.citation_count = author_ss.get("citationCount")
        m.paper_count = author_ss.get("paperCount")
        m.ss_url = f"https://www.semanticscholar.org/author/{aid}"
        m.name = author_ss.get("name", query)

        time.sleep(0.5)
        papers = ss_get_papers(aid)
        recent = [p for p in papers if (p.get("year") or 0) >= CURRENT_YEAR - RECENT_YEARS]
        m.papers_last_5y = len(recent)
        m.citations_last_5y = sum(p.get("citationCount", 0) for p in recent)
        if papers:
            top = max(papers, key=lambda p: p.get("citationCount", 0))
            m.top_paper_title = top.get("title")
            m.top_paper_citations = top.get("citationCount")
            m.top_paper_year = top.get("year")

    # 2. OpenAlex (real FWCI + OA)
    time.sleep(0.5)
    oa_author = oa_search_author(m.name if m.name != query else query)
    if oa_author:
        oa = oa_get_metrics(oa_author)
        m.oa_id = oa["oa_id"]
        m.fwci = oa["fwci"]
        m.h_index_oa = oa["h_index_oa"]
        m.oa_ratio = oa["oa_ratio"]
        m.works_count_oa = oa["works_count_oa"]
        m.top_concepts = oa["top_concepts"]
        m.oa_url = oa["oa_url"]

        # Use OpenAlex h-index as fallback if SS missing
        if m.h_index is None:
            m.h_index = m.h_index_oa

    return m


# === SCORING ===

def score_applicant(m: AuthorMetrics) -> dict:
    """
    Рейтинг 0-100 для reviewer4.
    v2.0: добавлен бонус за FWCI и OA-ratio.
    """
    score = 0
    flags = []
    h = m.h_index or m.h_index_oa or 0
    cit = m.citation_count or 0
    papers5 = m.papers_last_5y or 0
    fwci = m.fwci or 0
    oa_ratio = m.oa_ratio or 0

    # h-index (30%)
    if h >= 20:    score += 30; flags.append("h≥20 🏆")
    elif h >= 10:  score += 22; flags.append("h≥10 ✅")
    elif h >= 5:   score += 12
    elif h >= 1:   score += 5

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

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

    # FWCI реальный (20%) — OpenAlex
    if fwci >= 3.0:   score += 20; flags.append("FWCI≥3 📊")
    elif fwci >= 1.5: score += 15; flags.append("FWCI≥1.5")
    elif fwci >= 1.0: score += 10
    elif fwci > 0:    score += 5

    # OA ratio бонус (5%)
    if oa_ratio >= 0.7:   score += 5; flags.append("OA 🔓")
    elif oa_ratio >= 0.4: score += 3

    # Cross-field бонус (5%) — если concepts разнообразны
    if len(m.top_concepts) >= 4:
        score += 5; flags.append("cross-field 🌐")

    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,
        "h_index_ss": m.h_index,
        "h_index_oa": m.h_index_oa,
        "citation_count": cit,
        "paper_count": m.paper_count,
        "papers_last_5y": papers5,
        "citations_last_5y": m.citations_last_5y,
        "fwci": fwci if fwci else None,
        "oa_ratio": oa_ratio if oa_ratio else None,
        "top_concepts": m.top_concepts,
        "ss_url": m.ss_url,
        "oa_url": m.oa_url,
        "top_paper": m.top_paper_title,
        "top_paper_citations": m.top_paper_citations,
    }


# === BATCH ===

def evaluate_batch(names: list, ss_ids: list = None) -> tuple:
    raw_metrics, scores_list = [], []
    ss_ids = ss_ids or [None] * len(names)

    for name, ss_id in zip(names, ss_ids):
        label = name + (f" [{ss_id}]" if ss_id else "")
        print(f"  ▶ {label}")
        try:
            m = compute_metrics(name, ss_id)
            raw = asdict(m)
            raw_metrics.append(raw)
            s = score_applicant(m)
            scores_list.append(s)
            fwci_str = f"FWCI={m.fwci:.2f}" if m.fwci else "FWCI=?"
            print(f"    h={m.h_index}  cit={m.citation_count}  {fwci_str}  OA={m.oa_ratio}  → [{s['tier']}] {s['score']}/100")
        except Exception as e:
            print(f"    ERROR: {e}", file=sys.stderr)
            raw_metrics.append({"query": name, "name": name, "error": str(e)})

    scores_list.sort(key=lambda x: x["score"], reverse=True)
    return raw_metrics, scores_list


# === SAVE ===

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, path: Path):
    if not scores:
        return
    fields = ["score", "tier", "name", "h_index", "h_index_oa", "citation_count",
              "paper_count", "papers_last_5y", "citations_last_5y", "fwci",
              "oa_ratio", "top_concepts", "flags", "ss_url", "oa_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", [])),
                   "top_concepts": "; ".join(s.get("top_concepts", []))}
            w.writerow(row)
    print(f"  CSV → {path}")


def save_markdown(scores: list, raw: list, path: Path):
    lines = [
        "# Наукометрика reviewer4 v2.0 — Рейтинг авторов",
        f"*Дата: 2026-02-25 | Источники: Semantic Scholar + OpenAlex*",
        "",
        "## Рейтинг",
        "",
        "| # | Имя | Тир | Score | h | Цит | 5y | FWCI | OA% | Концепты |",
        "|---|-----|-----|-------|---|-----|----|------|-----|----------|",
    ]
    for i, s in enumerate(scores, 1):
        concepts = ", ".join(s.get("top_concepts", [])[:2])
        fwci = f"{s['fwci']:.2f}" if s.get("fwci") else "—"
        oa = f"{int((s.get('oa_ratio') or 0)*100)}%" if s.get("oa_ratio") else "—"
        flags = " ".join(s.get("flags", []))
        name_link = f"[{s['name']}]({s.get('ss_url') or '#'})"
        lines.append(
            f"| {i} | {name_link} | **{s['tier']}** | {s['score']}/100 "
            f"| {s['h_index']} | {s['citation_count']:,} | {s['papers_last_5y']} "
            f"| {fwci} | {oa} | {concepts} |"
        )

    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 += [
        "",
        "## Методология v2.0",
        "",
        "**Метрики:**",
        "- **h-index**: Semantic Scholar (fallback: OpenAlex)",
        "- **citations**: Semantic Scholar (агрегат всех источников)",
        "- **papers/cit last 5y**: Semantic Scholar, 2021-2026",
        "- **FWCI** (field-weighted): OpenAlex `mean_citations_per_work` — реальный, нормированный по области ✅",
        "- **OA ratio**: OpenAlex `oa_works_count / works_count` — % открытых публикаций",
        "- **Cross-field**: OpenAlex `x_concepts` — тематические концепты (бонус если ≥4)",
        "",
        "**Что НЕ доступно бесплатно:**",
        "- Disruption index (CD): только датасет ~49M papers, не API",
        "- SCImago Q1: нет API (портал только)",
        "- FWCI по годам (тренд): только текущий",
        "- Citation burst: нет публичного API",
        "",
        "**Score = h(30%) + cit(20%) + activity5y(20%) + FWCI(20%) + OA-bonus(5%) + cross-field(5%)**",
        "**Тир**: A(≥70), B(40-69), C(<40)",
        "",
        f"*Сгенерировано Феанором, reviewer4 v2.0*",
    ]

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


# === TEST AUTHORS ===

TEST_AUTHORS = [
    "Ivan Oseledets",
    "Stephen Boyd",
    "Dmitry Vetrov",
    "Victor Lempitsky",
    "Andrej Karpathy",
]

KNOWN_IDS = {
    "Ivan Oseledets": "1738205",
    "Andrej Karpathy": "1723755",
    "Yann LeCun": "1688882",
}


def main():
    parser = argparse.ArgumentParser(description="Наукометрика reviewer4 v2.0")
    parser.add_argument("--names", nargs="+")
    parser.add_argument("--ids", nargs="+", help="Semantic Scholar IDs")
    parser.add_argument("--output-dir", default="data")
    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 v2.0 ===")
    print(f"Авторов: {len(names)} | Sources: Semantic Scholar + OpenAlex")
    print()

    raw, scores = evaluate_batch(names, ss_ids)

    print("\n=== Рейтинг ===")
    for s in scores:
        flags_str = "  " + " ".join(s["flags"]) if s["flags"] else ""
        fwci_str = f"FWCI={s['fwci']:.1f}" if s.get("fwci") else "FWCI=—"
        print(f"  [{s['tier']}] {s['score']:3d}/100  {s['name']:<30s}  h={s['h_index']:<4}  {fwci_str:<10}{flags_str}")

    print("\n=== Сохранение ===")
    payload = {
        "version": "2.0",
        "date": "2026-02-25",
        "sources": ["Semantic Scholar", "OpenAlex"],
        "authors_count": len(names),
        "raw_metrics": raw,
        "ranking": scores,
        "new_in_v2": ["FWCI (OpenAlex, real)", "OA ratio", "Cross-field concepts"],
        "not_available_free": ["Disruption index (CD)", "SCImago Q1", "FWCI trend", "Citation burst"],
    }
    save_json(payload, out_dir / "naukometrika_v2_results.json")
    save_csv(scores, out_dir / "naukometrika_v2_results.csv")
    save_markdown(scores, raw, out_dir / "naukometrika_v2_report.md")
    print("\nГотово! v2.0")


if __name__ == "__main__":
    main()
