#!/usr/bin/env python3
from __future__ import annotations

import json
import sys
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy import stats


ROOT = Path(__file__).resolve().parent
DATA = ROOT / "trending-top20-2026-08-29.csv"
OUT = ROOT / "outputs"
SKILL_SCRIPTS = ROOT / ".agents" / "skills" / "scientific-visualization" / "scripts"
sys.path.insert(0, str(SKILL_SCRIPTS))

from figure_export import export_figure  # noqa: E402


def percentile_ci(values: np.ndarray, level: float = 0.95) -> tuple[float, float]:
    tail = (1 - level) / 2
    return tuple(np.quantile(values, [tail, 1 - tail]).tolist())


def main() -> None:
    OUT.mkdir(exist_ok=True)
    frame = pd.read_csv(DATA)
    required = {"rank", "repository", "total_stars", "stars_today"}
    missing = required - set(frame.columns)
    if missing:
        raise ValueError(f"missing columns: {sorted(missing)}")
    if len(frame) != 20 or frame["rank"].tolist() != list(range(1, 21)):
        raise ValueError("expected a complete ranked top-20 snapshot")
    if (frame[["total_stars", "stars_today"]] <= 0).any().any():
        raise ValueError("star counts must be positive")

    frame["momentum_pct"] = frame["stars_today"] / frame["total_stars"] * 100
    frame["short_name"] = frame["repository"].str.split("/").str[-1]
    rho, p_value = stats.spearmanr(frame["total_stars"], frame["stars_today"])

    rng = np.random.default_rng(20260829)
    boot = np.empty(10_000)
    x = frame["total_stars"].to_numpy()
    y = frame["stars_today"].to_numpy()
    for i in range(len(boot)):
        idx = rng.integers(0, len(frame), len(frame))
        if len(np.unique(x[idx])) < 2 or len(np.unique(y[idx])) < 2:
            boot[i] = np.nan
        else:
            boot[i] = stats.spearmanr(x[idx], y[idx]).statistic
    boot = boot[~np.isnan(boot)]
    ci_low, ci_high = percentile_ci(boot)

    shapiro_total = stats.shapiro(np.log10(frame["total_stars"]))
    shapiro_today = stats.shapiro(np.log10(frame["stars_today"]))

    top_momentum = frame.nlargest(5, "momentum_pct")[[
        "rank", "repository", "total_stars", "stars_today", "momentum_pct"
    ]]
    summary = {
        "snapshot": "GitHub Trending global Today",
        "captured_at_utc8": frame["captured_at_utc8"].iloc[0],
        "n": int(len(frame)),
        "planned_test": "two-sided Spearman rank correlation",
        "spearman_rho": float(rho),
        "p_value": float(p_value),
        "bootstrap_iterations": int(len(boot)),
        "bootstrap_seed": 20260829,
        "rho_ci95_percentile": [float(ci_low), float(ci_high)],
        "normality_diagnostic_log10": {
            "total_stars": {"W": float(shapiro_total.statistic), "p": float(shapiro_total.pvalue)},
            "stars_today": {"W": float(shapiro_today.statistic), "p": float(shapiro_today.pvalue)},
        },
        "top_momentum": top_momentum.to_dict(orient="records"),
        "interpretation_boundary": "A one-day attention snapshot; no causal or long-term quality claim.",
    }
    (OUT / "statistical-summary.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8"
    )
    frame.to_csv(OUT / "trending-top20-with-momentum.csv", index=False)

    plt.rcParams.update({
        "font.family": "DejaVu Sans",
        "axes.titleweight": "bold",
        "axes.spines.top": False,
        "axes.spines.right": False,
    })
    fig, axes = plt.subplot_mosaic(
        [["scatter", "bars"], ["key", "key"]],
        figsize=(12.8, 8.0),
        height_ratios=[4.6, 1.25],
        layout="constrained",
    )
    ax1, ax2, ax_key = axes["scatter"], axes["bars"], axes["key"]

    ax1.scatter(
        frame["total_stars"], frame["stars_today"],
        s=70, color="#0072B2", edgecolor="white", linewidth=0.8, alpha=0.9,
    )
    for row in frame.itertuples():
        ax1.annotate(
            str(row.rank),
            (row.total_stars, row.stars_today),
            xytext=(4, 4),
            textcoords="offset points",
            fontsize=7,
            fontweight="bold",
        )
    ax1.set_xscale("log")
    ax1.set_xlabel("Total stars (log scale)")
    ax1.set_ylabel("Stars today")
    ax1.set_title("Accumulated popularity vs. today's attention")
    ax1.grid(True, alpha=0.18)
    ax1.text(
        0.02, 0.98,
        f"Spearman ρ = {rho:.2f}\np = {p_value:.3f}\n95% bootstrap CI [{ci_low:.2f}, {ci_high:.2f}]",
        transform=ax1.transAxes, va="top", fontsize=9,
        bbox={"boxstyle": "round,pad=0.4", "facecolor": "white", "edgecolor": "#999999"},
    )

    ranked = frame.sort_values("momentum_pct", ascending=True).tail(10)
    colors = ["#E69F00" if repo == "K-Dense-AI/scientific-agent-skills" else "#009E73"
              for repo in ranked["repository"]]
    ax2.barh(ranked["short_name"], ranked["momentum_pct"], color=colors)
    ax2.set_xlabel("Stars today / total stars (%)")
    ax2.set_title("Top relative momentum in the same snapshot")
    ax2.grid(True, axis="x", alpha=0.18)
    for y_pos, value in enumerate(ranked["momentum_pct"]):
        ax2.text(value, y_pos, f" {value:.1f}%", va="center", fontsize=8)

    ax_key.axis("off")
    key_rows = frame[["rank", "repository"]].to_records(index=False)
    left = "\n".join(f"{rank:>2}. {repo}" for rank, repo in key_rows[:10])
    right = "\n".join(f"{rank:>2}. {repo}" for rank, repo in key_rows[10:])
    ax_key.text(0.01, 0.98, left, va="top", ha="left", fontsize=7.2, family="monospace")
    ax_key.text(0.51, 0.98, right, va="top", ha="left", fontsize=7.2, family="monospace")
    ax_key.set_title("Point key — rank and repository", loc="left", fontsize=10, fontweight="bold", pad=2)

    fig.suptitle("GitHub Trending Today: a 20-repository snapshot", fontsize=16, fontweight="bold")
    fig.text(
        0.5,
        0.004,
        "Captured 2026-08-29 15:02 UTC+8. Dynamic ranking; not a quality or causality measure.",
        ha="center",
        fontsize=8,
        color="#555555",
    )
    export_figure(
        fig,
        OUT / "github-trending-scientific-analysis",
        formats=["png", "svg"],
        dpi=180,
        bbox_inches=None,
        facecolor="white",
        overwrite=True,
        write_manifest=True,
        provenance={
            "raw_data": DATA.name,
            "analysis_plan": "analysis-plan.md",
            "transformations": ["momentum_pct = stars_today / total_stars * 100", "log10 x-axis display only"],
            "statistics": "Two-sided Spearman rho; 10,000 percentile bootstrap samples; seed 20260829",
            "source": "https://github.com/trending?since=daily",
        },
    )
    plt.close(fig)

    momentum_lines = [
        "| 排名 | 仓库 | 累计 Star | 当天新增 | 相对动量 |",
        "|---:|---|---:|---:|---:|",
    ]
    for row in top_momentum.itertuples(index=False):
        momentum_lines.append(
            f"| {row.rank} | {row.repository} | {row.total_stars:,} | "
            f"{row.stars_today:,} | {row.momentum_pct:.2f}% |"
        )
    momentum_table = "\n".join(momentum_lines)
    report = f"""# Scientific Agent Skills 实测结果\n\n## 一句话结论\n\n在 2026-08-29 的 GitHub Trending 全球 Today 前 20 项快照中，累计 Star 与当天新增 Star 的 Spearman 相关为 {rho:.2f}，双侧 p={p_value:.3f}，10,000 次 bootstrap 的 95% 区间为 [{ci_low:.2f}, {ci_high:.2f}]。这个样本没有给出稳定的单调关系证据，老牌高 Star 仓库不必然在当天增长更快。\n\n## 真实任务\n\n使用 Scientific Agent Skills 中的 `statistical-analysis` 与 `scientific-visualization`，分析剑的 AI 实验室当天雷达抓到的 20 个 Trending 项目。原始表保持不变，辅助指标只计算 `当天新增 / 累计 Star`。\n\n## 方法\n\n- 预先选择双侧 Spearman 等级相关，不假设线性和正态。\n- 保留 20 个完整排名，无缺失、无删点。\n- 以固定随机种子 20260829 做 10,000 次 bootstrap。\n- 累计 Star 只在图上使用对数坐标，没有对检验结果做变换。\n- 图表同时导出 PNG、SVG 和机器可读 provenance manifest。\n\n## 相对动量前五\n\n{momentum_table}\n\n## 边界\n\n这是单日动态快照。样本由 Trending 机制筛选，不满足随机抽样；p 值和区间只能描述这 20 个项目，不能外推为 GitHub 全站规律，也不能把当天增长解释为项目质量。\n"""
    (OUT / "experiment-result.md").write_text(report, encoding="utf-8")

    for path in sorted(OUT.iterdir()):
        if path.is_file():
            print(f"{path.name}\t{path.stat().st_size}")
    print(json.dumps(summary, ensure_ascii=False))


if __name__ == "__main__":
    main()
