"""Compare iter100 vs iter150 checkpoints: action_mean diff and refine_mask equality."""
import numpy as np
import torch
from torch_geometric.data import Batch

from src.network import create_model
from src.utils import load_checkpoint, setup_helmholtz_config


def load_config():
    from src.utils import load_config as _lc
    from pathlib import Path
    cfg_path = Path(__file__).resolve().parent / "src" / "config.yaml"
    return _lc(str(cfg_path))


def compare_checkpoints(ckpt_a, ckpt_b, label_a="iter100", label_b="iter150"):
    config = load_config()
    setup_helmholtz_config(config)
    algo = config.get("algorithm", {})

    from environment.mesh_refinement import MeshRefinement

    env = MeshRefinement(
        environment_config=config.get("environment", {}).get("mesh_refinement", {}),
        seed=99,
    )

    # ── Load both models ──
    model_a = create_model(env, config.get("network", {}), algo.get("ppo", {}))
    load_checkpoint(model_a, ckpt_a)
    model_a.eval()

    model_b = create_model(env, config.get("network", {}), algo.get("ppo", {}))
    load_checkpoint(model_b, ckpt_b)
    model_b.eval()

    # ── Get same initial observation ──
    env.reset()
    obs = env.reset()  # second reset ensures same state

    with torch.no_grad():
        batch = Batch.from_data_list([obs])

        # Model A
        shared_a, batch_a = model_a._encode(batch)
        latent_pi_a = model_a.policy_mlp(shared_a)
        action_mean_a = model_a.action_out(latent_pi_a).cpu().numpy().flatten()
        dist_a = model_a._make_distribution(latent_pi_a)
        actions_a = dist_a.get_actions(deterministic=True).cpu().numpy().flatten()

        # Model B
        shared_b, batch_b = model_b._encode(batch)
        latent_pi_b = model_b.policy_mlp(shared_b)
        action_mean_b = model_b.action_out(latent_pi_b).cpu().numpy().flatten()
        dist_b = model_b._make_distribution(latent_pi_b)
        actions_b = dist_b.get_actions(deterministic=True).cpu().numpy().flatten()

    # ── Compare action_mean ──
    diff = action_mean_a - action_mean_b
    print(f"\n{'='*60}")
    print(f"  1. action_mean comparison")
    print(f"{'='*60}")
    print(f"  {label_a} action_mean:  min={action_mean_a.min():.6f}  max={action_mean_a.max():.6f}  mean={action_mean_a.mean():.6f}  std={action_mean_a.std():.6f}")
    print(f"  {label_b} action_mean:  min={action_mean_b.min():.6f}  max={action_mean_b.max():.6f}  mean={action_mean_b.mean():.6f}  std={action_mean_b.std():.6f}")
    print(f"  ---")
    print(f"  |diff|:              min={np.abs(diff).min():.8f}  max={np.abs(diff).max():.8f}  mean={np.abs(diff).mean():.8f}")
    print(f"  diff = 0 exactly:    {int(np.sum(diff == 0))} / {len(diff)}  ({100 * np.sum(diff == 0) / len(diff):.2f}%)")
    print(f"  |diff| < 1e-6:       {int(np.sum(np.abs(diff) < 1e-6))} / {len(diff)}")
    print(f"  |diff| < 1e-4:       {int(np.sum(np.abs(diff) < 1e-4))} / {len(diff)}")
    print(f"  cos similarity:      {np.dot(action_mean_a, action_mean_b) / (np.linalg.norm(action_mean_a) * np.linalg.norm(action_mean_b) + 1e-12):.8f}")

    # ── Compare refine_mask (action > 0) ──
    mask_a = actions_a > 0.0
    mask_b = actions_b > 0.0
    mask_equal = np.array_equal(mask_a, mask_b)

    print(f"\n{'='*60}")
    print(f"  2. refine_mask comparison")
    print(f"{'='*60}")
    print(f"  {label_a} refine_mask: sum={mask_a.sum()} / {len(mask_a)}  ({100 * mask_a.sum() / len(mask_a):.1f}%)")
    print(f"  {label_b} refine_mask: sum={mask_b.sum()} / {len(mask_b)}  ({100 * mask_b.sum() / len(mask_b):.1f}%)")
    print(f"  refine_mask exactly equal: {mask_equal}")
    print(f"  mask XOR sum:              {(mask_a ^ mask_b).sum()} / {len(mask_a)}")

    if not mask_equal:
        diff_idx = np.where(mask_a != mask_b)[0]
        print(f"  First 20 differing indices: {diff_idx[:20].tolist()}")
        print(f"  At those indices, {label_a} action_mean: {action_mean_a[diff_idx[:10]]}")
        print(f"  At those indices, {label_b} action_mean: {action_mean_b[diff_idx[:10]]}")

    # ── 3. Parameter-level diff ──
    print(f"\n{'='*60}")
    print(f"  3. Model parameter weight diff (L2 norm)")
    print(f"{'='*60}")
    sd_a = torch.load(ckpt_a, map_location="cpu")["model_state_dict"]
    sd_b = torch.load(ckpt_b, map_location="cpu")["model_state_dict"]
    for k in sorted(sd_a.keys()):
        w_a = sd_a[k].float()
        w_b = sd_b[k].float()
        l2 = torch.norm(w_a - w_b).item()
        rel = l2 / (torch.norm(w_a).item() + 1e-12)
        print(f"  {k:55s}  |Δ|₂={l2:.6e}  rel={rel:.6e}")


if __name__ == "__main__":
    import sys
    d1 = sys.argv[1] if len(sys.argv) > 1 else "checkpoints/model_iter0100.pt"
    d2 = sys.argv[2] if len(sys.argv) > 2 else "checkpoints/model_iter0150.pt"
    l1 = sys.argv[3] if len(sys.argv) > 3 else "iter100"
    l2 = sys.argv[4] if len(sys.argv) > 4 else "iter150"
    compare_checkpoints(d1, d2, l1, l2)
