"""Figures and numbers for the how-it-works page, from the built-in sample room.

    python -m roommap.figures        # writes out/*.png and out/results.json, prints the numbers

out/sample_map.png     the map of the sample room, coloured by group, with the group names
out/silhouette.png     mean silhouette for k = 2..8 (how k is chosen)
out/tsne_progress.png  KL divergence and trustworthiness over the t-SNE iterations
out/similarity.png     cosine similarity of pairs inside a group and across groups
out/pca_vs_tsne.png    the same room laid out by PCA alone and by t-SNE, with trustworthiness
out/chaos.png          two t-SNE runs from inputs that differ by 1e-12: same quality, different picture
"""
from __future__ import annotations

import json
import os
import time

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np

from . import cluster, embed, metrics, pca, tsne
from .sample import SAMPLE

OUT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "out")
COLORS = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#6250d6", "#8a6d3b"]


def style(ax):
    for s in ("top", "right"):
        ax.spines[s].set_visible(False)
    ax.grid(True, color="#e3e6ec", lw=0.8)
    ax.set_axisbelow(True)


def main() -> dict:
    os.makedirs(OUT, exist_ok=True)
    texts = SAMPLE
    res: dict = {"n": len(texts)}
    t0 = time.perf_counter()
    X = pca.normalise_rows(embed.embed_all(texts))
    res["embed_ms"] = (time.perf_counter() - t0) * 1e3
    t0 = time.perf_counter()
    pcs, explained = pca.pca(X, 10)
    res["pca_ms"] = (time.perf_counter() - t0) * 1e3
    res["explained_10"] = float(explained.sum())
    res["explained_2"] = float(explained[:2].sum())

    # t-SNE with progress
    P = tsne.affinities(tsne.squared_distances(pcs), tsne.default_perplexity(len(texts)))
    t = tsne.TSNE(P, tsne.initial_positions(pcs))
    its, kls, trusts = [], [], []
    t0 = time.perf_counter()
    for it in range(0, tsne.ITERS, 10):
        t.step(10)
        its.append(t.it); kls.append(t.kl()); trusts.append(metrics.trustworthiness(pcs, t.Y, 5))
    res["tsne_ms"] = (time.perf_counter() - t0) * 1e3 * 0  # measured again below without the metrics
    t0 = time.perf_counter(); Y, kl = tsne.layout(pcs); res["tsne_ms"] = (time.perf_counter() - t0) * 1e3
    res["kl"] = kl
    res["trust_tsne"] = metrics.trustworthiness(pcs, Y, 5)
    res["pres_tsne"] = metrics.neighbourhood_preservation(pcs, Y, 5)
    res["trust_tsne_vs_full"] = metrics.trustworthiness(X, Y, 5)
    Ypca = pcs[:, :2]
    res["trust_pca"] = metrics.trustworthiness(pcs, Ypca, 5)
    res["pres_pca"] = metrics.neighbourhood_preservation(pcs, Ypca, 5)
    res["trust_tsne_k10"] = metrics.trustworthiness(pcs, Y, 10)
    res["trust_pca_k10"] = metrics.trustworthiness(pcs, Ypca, 10)

    t0 = time.perf_counter()
    k, labels, scores = cluster.choose_k(pcs)
    names = cluster.name_clusters(texts, labels)
    res["cluster_ms"] = (time.perf_counter() - t0) * 1e3
    res["k"] = k; res["names"] = names; res["silhouettes"] = scores
    res["sizes"] = [int((labels == c).sum()) for c in range(k)]

    # similarity inside vs across groups
    S = X @ X.T
    iu = np.triu_indices(len(texts), 1)
    same = labels[iu[0]] == labels[iu[1]]
    sims = S[iu]
    res["sim_same_mean"] = float(sims[same].mean()); res["sim_diff_mean"] = float(sims[~same].mean())
    res["sim_same_median"] = float(np.median(sims[same])); res["sim_diff_median"] = float(np.median(sims[~same]))
    nn = [metrics.knn(metrics.pairwise_sq(pcs), 5)[i] for i in range(len(texts))]
    res["nn_same_group"] = float(np.mean([np.mean(labels[nn[i]] == labels[i]) for i in range(len(texts))]))

    # chaos: perturb by 1e-12
    rng = np.random.default_rng(0)
    Y2, kl2 = tsne.layout(pcs + rng.normal(scale=1e-12, size=pcs.shape))
    n1, n2 = metrics.knn(metrics.pairwise_sq(Y), 5), metrics.knn(metrics.pairwise_sq(Y2), 5)
    res["chaos_kl"] = kl2; res["chaos_trust"] = metrics.trustworthiness(pcs, Y2, 5)
    res["chaos_shared_neighbours"] = float(np.mean([len(set(n1[i]) & set(n2[i])) / 5 for i in range(len(texts))]))

    # ---- figures
    def scatter(ax, Yp, title, with_names=True):
        for c in range(k):
            m = labels == c
            ax.scatter(Yp[m, 0], Yp[m, 1], s=42, color=COLORS[c % 8], edgecolor="white", lw=0.8, zorder=3)
            if with_names:
                mx, my = np.median(Yp[m, 0]), np.median(Yp[m, 1])
                ax.text(mx, my, names[c], ha="center", va="center", fontsize=9, fontweight="bold", color=COLORS[c % 8],
                        bbox=dict(boxstyle="round,pad=0.3", fc="white", ec=COLORS[c % 8], lw=1.2, alpha=0.92), zorder=4)
        ax.set_title(title, fontsize=11, loc="left")
        ax.set_xticks([]); ax.set_yticks([])
        for s in ax.spines.values():
            s.set_color("#d8dde6")

    fig, ax = plt.subplots(figsize=(8, 6))
    scatter(ax, Y, f"The sample room: 60 phrases, {k} groups (fallback embedder, t-SNE)")
    for i, s in enumerate(texts):
        ax.annotate(s if len(s) < 30 else s[:28] + "…", (Y[i, 0], Y[i, 1]), xytext=(5, 3), textcoords="offset points", fontsize=5.5, color="#5a6577", zorder=2)
    fig.tight_layout(); fig.savefig(os.path.join(OUT, "sample_map.png"), dpi=130); plt.close(fig)

    fig, ax = plt.subplots(figsize=(6, 3.6))
    ks, ss = zip(*scores)
    ax.plot(ks, ss, "o-", color="#2153D0"); ax.plot([k], [dict(scores)[k]], "o", ms=12, mfc="none", mec="#C98A00", mew=2)
    ax.set_xlabel("number of groups k"); ax.set_ylabel("mean silhouette"); ax.set_title("Choosing k: best silhouette wins", loc="left", fontsize=11)
    style(ax); fig.tight_layout(); fig.savefig(os.path.join(OUT, "silhouette.png"), dpi=130); plt.close(fig)

    fig, axes = plt.subplots(1, 2, figsize=(10, 3.6))
    axes[0].plot(its, kls, color="#2153D0"); axes[0].axvline(tsne.EARLY_ITERS, color="#C98A00", ls="--", lw=1)
    axes[0].set_xlabel("iteration"); axes[0].set_ylabel("KL(P ‖ Q)"); axes[0].set_title("t-SNE objective (early exaggeration ends at 100)", loc="left", fontsize=11)
    axes[1].plot(its, trusts, color="#0E8C7A"); axes[1].axhline(res["trust_pca"], color="#5a6577", ls=":", lw=1)
    axes[1].text(its[-1], res["trust_pca"], " PCA alone", va="center", fontsize=8, color="#5a6577")
    axes[1].set_xlabel("iteration"); axes[1].set_ylabel("trustworthiness (k = 5)"); axes[1].set_title("How many map neighbours are real neighbours", loc="left", fontsize=11)
    for ax in axes:
        style(ax)
    fig.tight_layout(); fig.savefig(os.path.join(OUT, "tsne_progress.png"), dpi=130); plt.close(fig)

    fig, ax = plt.subplots(figsize=(6, 3.6))
    bins = np.linspace(-0.1, 1, 34)
    ax.hist(sims[~same], bins=bins, color="#d8dde6", label=f"different groups (median {res['sim_diff_median']:.2f})")
    ax.hist(sims[same], bins=bins, color="#2153D0", alpha=0.85, label=f"same group (median {res['sim_same_median']:.2f})")
    ax.set_xlabel("cosine similarity of a pair of phrases"); ax.set_ylabel("pairs"); ax.legend(frameon=False, fontsize=9)
    ax.set_title("What “similar” looks like in numbers", loc="left", fontsize=11)
    style(ax); fig.tight_layout(); fig.savefig(os.path.join(OUT, "similarity.png"), dpi=130); plt.close(fig)

    fig, axes = plt.subplots(1, 2, figsize=(11, 4.8))
    scatter(axes[0], Ypca, f"PCA to 2-D: trustworthiness {res['trust_pca']:.2f}, neighbours kept {res['pres_pca']:.0%}", with_names=False)
    scatter(axes[1], Y, f"PCA to 10-D then t-SNE: trustworthiness {res['trust_tsne']:.2f}, neighbours kept {res['pres_tsne']:.0%}", with_names=False)
    fig.tight_layout(); fig.savefig(os.path.join(OUT, "pca_vs_tsne.png"), dpi=130); plt.close(fig)

    fig, axes = plt.subplots(1, 2, figsize=(11, 4.8))
    scatter(axes[0], Y, f"Run A: KL {kl:.3f}, trustworthiness {res['trust_tsne']:.2f}")
    scatter(axes[1], Y2, f"Run B (inputs nudged by 1e-12): KL {kl2:.3f}, trustworthiness {res['chaos_trust']:.2f}")
    fig.suptitle(f"Same room, two t-SNE runs: {res['chaos_shared_neighbours']:.0%} of each point's 5 map neighbours are shared", fontsize=11)
    fig.tight_layout(); fig.savefig(os.path.join(OUT, "chaos.png"), dpi=130); plt.close(fig)

    with open(os.path.join(OUT, "results.json"), "w", encoding="utf-8") as f:
        json.dump(res, f, indent=1)
    return res


if __name__ == "__main__":
    r = main()
    for key, v in r.items():
        print(f"{key}: {v}")
