"""Build the results_summary.json report from the completed BFF experiment."""

import json
import os
import subprocess
import sys
import time
from pathlib import Path

import numpy as np

from abiogenesis.metrics import (
    compression_ratio,
    find_replicators,
    find_repeated_subsequences,
    top_sequences,
    population_histogram,
    ReplicatorType,
)

RUN_DIR = Path("/Users/motherlabs/Desktop/mlabs/data/bff_runs")
FINAL_META = RUN_DIR / "final_10000000_meta.json"
FINAL_NPZ = RUN_DIR / "final_10000000.npz"

# ──────────────────────────────────────────────
# Load data
# ──────────────────────────────────────────────
print("Loading final checkpoint...")
with open(FINAL_META) as f:
    meta = json.load(f)

final_data = np.load(str(FINAL_NPZ))
tapes = final_data["tapes"]
ops_log = final_data["ops_log"]

snap_interactions = meta["snapshot_interactions"]
snap_compression = meta["snapshot_compression"]
snap_mean_ops = meta["snapshot_mean_ops"]
snap_replicator_counts = meta["snapshot_replicator_counts"]
config = meta["config"]

# ──────────────────────────────────────────────
# Try to load initial state from the 100k checkpoint for "interaction 0" tapes
# ──────────────────────────────────────────────
# We don't have interaction 0 saved, but the earliest checkpoint
# has already diverged. We'll note this.

# ──────────────────────────────────────────────
# 1. PHASE TRANSITION
# ──────────────────────────────────────────────
print("Building phase transition data...")

# Build full timeseries at checkpoint granularity (every 100k)
# We have snapshots every 10k, use those
timeseries = []
for i, (inter, comp, mops) in enumerate(zip(snap_interactions, snap_compression, snap_mean_ops)):
    timeseries.append({
        "interaction": inter,
        "compression": round(comp, 6),
        "mean_ops": round(mops, 2),
    })

# Find threshold crossings
first_below_015 = None
first_below_010 = None
min_compression = float("inf")
min_compression_interaction = 0

for i, (inter, comp) in enumerate(zip(snap_interactions, snap_compression)):
    if comp < 0.15 and first_below_015 is None:
        first_below_015 = inter
    if comp < 0.10 and first_below_010 is None:
        first_below_010 = inter
    if comp < min_compression:
        min_compression = comp
        min_compression_interaction = inter

# Compute rate from wall clock via checkpoint file timestamps
checkpoint_times = {}
for p in sorted(RUN_DIR.glob("checkpoint_*_meta.json")):
    name = p.stem.replace("_meta", "")
    inter_num = int(name.replace("checkpoint_", ""))
    checkpoint_times[inter_num] = p.stat().st_mtime

# Also get final
final_mtime = FINAL_META.stat().st_mtime
checkpoint_times[10000000] = final_mtime

sorted_checkpoints = sorted(checkpoint_times.items())
if len(sorted_checkpoints) >= 2:
    total_wall_clock = sorted_checkpoints[-1][1] - sorted_checkpoints[0][1]
    # Add time for the first 100k (estimate from first interval)
    if len(sorted_checkpoints) >= 3:
        first_interval = sorted_checkpoints[1][1] - sorted_checkpoints[0][1]
        total_wall_clock += first_interval  # rough estimate for 0->100k
else:
    total_wall_clock = 0

# Add rate to timeseries using checkpoint timestamps
for entry in timeseries:
    inter = entry["interaction"]
    # Find bounding checkpoints for rate estimation
    prev_t = None
    prev_i = None
    for ci, ct in sorted_checkpoints:
        if ci <= inter:
            prev_t = ct
            prev_i = ci
        if ci >= inter and prev_t is not None:
            if ci > prev_i:
                entry["rate"] = round((ci - prev_i) / (ct - prev_t), 0)
            break

phase_transition = {
    "timeseries_every_10k": timeseries,
    "first_below_0.15": first_below_015,
    "first_below_0.10": first_below_010,
    "minimum_compression": round(min_compression, 6),
    "minimum_compression_at_interaction": min_compression_interaction,
}

# ──────────────────────────────────────────────
# 2. REPLICATOR CENSUS
# ──────────────────────────────────────────────
print("Running replicator census on final soup state...")
replicators = find_replicators(tapes, min_length=4, min_count=2)

# Build top 50
top_replicators = []
for r in replicators[:50]:
    top_replicators.append({
        "sequence_hex": r.sequence.hex(),
        "length": r.length,
        "population_count": r.count,
        "classification": r.classification.value,
    })

# Count by type
type_counts = {"inanimate": 0, "viral": 0, "cellular": 0}
for r in replicators:
    type_counts[r.classification.value] += 1

# Most populous
most_populous = None
if replicators:
    r = replicators[0]
    most_populous = {
        "sequence_hex": r.sequence.hex(),
        "length": r.length,
        "population_count": r.count,
        "classification": r.classification.value,
    }

# Population histogram (top tapes)
top_tape_list = top_sequences(tapes, n=20)
pop_hist = []
for seq_bytes, count in top_tape_list:
    pop_hist.append({
        "tape_hex": seq_bytes.hex(),
        "count": count,
    })

replicator_census = {
    "total_unique_replicators": len(replicators),
    "by_type": type_counts,
    "most_populous_replicator": most_populous,
    "top_50_replicators": top_replicators,
    "top_20_tapes_by_population": pop_hist,
}

# ──────────────────────────────────────────────
# 3. SYMBIOGENESIS EVENTS
# ──────────────────────────────────────────────
print("Analyzing symbiogenesis from ops log...")

# We don't have per-interaction merger tracking (would need to instrument soup.interact).
# What we CAN extract: ops distribution over time showing when complex interactions began.
# "Merger events" = interactions where ops > threshold (indicating deep execution)

ops_array = ops_log.astype(np.int32)
high_ops_threshold = 5000  # interactions hitting >5k ops indicate deep programs running
total_high_ops = int(np.sum(ops_array >= high_ops_threshold))
total_max_ops = int(np.sum(ops_array >= 10000))
total_nonzero = int(np.count_nonzero(ops_array))

# Ops distribution histogram
bins = [0, 1, 100, 500, 1000, 2000, 5000, 10000, 10001]
hist_counts, _ = np.histogram(ops_array, bins=bins)
ops_distribution = {}
labels = ["0 (inert)", "1-99", "100-499", "500-999", "1000-1999", "2000-4999", "5000-9999", "10000 (capped)"]
for label, count in zip(labels, hist_counts):
    ops_distribution[label] = int(count)

# Find first interaction hitting max ops
first_max_ops_idx = None
max_ops_indices = np.where(ops_array >= 10000)[0]
if len(max_ops_indices) > 0:
    first_max_ops_idx = int(max_ops_indices[0])

# Window-based ops analysis: mean ops in 100k windows
window_size = 100_000
n_windows = len(ops_array) // window_size
window_means = []
window_maxes = []
for w in range(n_windows):
    chunk = ops_array[w * window_size:(w + 1) * window_size]
    window_means.append(round(float(chunk.mean()), 2))
    window_maxes.append(int(chunk.max()))

# Replicator emergence timeline from snapshots
replicator_timeline = []
for i, (inter, rep_counts) in enumerate(zip(snap_interactions, snap_replicator_counts)):
    if inter % 100_000 == 0:  # every 100k
        replicator_timeline.append({
            "interaction": inter,
            "inanimate": rep_counts.get("inanimate", 0),
            "viral": rep_counts.get("viral", 0),
            "cellular": rep_counts.get("cellular", 0),
        })

symbiogenesis = {
    "note": "Per-interaction merger tracking not instrumented. Using ops distribution as proxy.",
    "total_interactions": int(len(ops_array)),
    "total_nonzero_ops": total_nonzero,
    "total_high_ops_above_5000": total_high_ops,
    "total_capped_at_10000": total_max_ops,
    "first_interaction_hitting_10000_ops": first_max_ops_idx,
    "ops_distribution": ops_distribution,
    "mean_ops_per_100k_window": window_means,
    "max_ops_per_100k_window": window_maxes,
    "replicator_emergence_timeline": replicator_timeline,
}

# ──────────────────────────────────────────────
# 4. SOUP SNAPSHOTS
# ──────────────────────────────────────────────
print("Collecting soup snapshots...")

# We don't have interaction 0 saved separately.
# Use first checkpoint (100k) as earliest available
earliest_npz = np.load(str(RUN_DIR / "checkpoint_100000.npz"))
earliest_tapes = earliest_npz["tapes"]

# Find the transition point — where compression first drops sharply
# Use the point where compression first went below 0.15
transition_interaction = first_below_015
transition_tapes = None
if transition_interaction:
    # Find nearest checkpoint
    nearest_ckpt = (transition_interaction // 100_000) * 100_000
    if nearest_ckpt == 0:
        nearest_ckpt = 100_000
    ckpt_path = RUN_DIR / f"checkpoint_{nearest_ckpt}.npz"
    if ckpt_path.exists():
        td = np.load(str(ckpt_path))
        transition_tapes = td["tapes"]
    else:
        # Try final
        transition_tapes = tapes

soup_snapshots = {
    "earliest_available": {
        "interaction": 100_000,
        "note": "Interaction 0 not checkpointed; this is the first checkpoint",
        "first_10_tapes_hex": [earliest_tapes[i].tobytes().hex() for i in range(10)],
    },
    "transition_point": {
        "interaction": transition_interaction,
        "compression_at_transition": None,
        "first_10_tapes_hex": [],
    },
    "final_state": {
        "interaction": 10_000_000,
        "first_10_tapes_hex": [tapes[i].tobytes().hex() for i in range(10)],
    },
}

if transition_tapes is not None:
    soup_snapshots["transition_point"]["first_10_tapes_hex"] = [
        transition_tapes[i].tobytes().hex() for i in range(10)
    ]
    # Find compression at that snapshot
    for inter, comp in zip(snap_interactions, snap_compression):
        if inter == transition_interaction:
            soup_snapshots["transition_point"]["compression_at_transition"] = round(comp, 6)
            break

# ──────────────────────────────────────────────
# 5. COMPARISON TO RUN 1
# ──────────────────────────────────────────────
print("Building run comparison...")

# Run 1 data captured during monitoring (pre-fix, strict bracket matching)
# From the monitoring session we captured these exact values:
run1_data = {
    100_000: 0.7144,
    200_000: 0.6179,
    300_000: 0.5269,
    400_000: 0.4931,
    500_000: 0.4657,
    600_000: 0.4455,
    700_000: 0.4346,
    800_000: 0.4268,
    900_000: 0.4199,
}

run1_min_compression = min(run1_data.values())

comparison_table = []
for inter in sorted(run1_data.keys()):
    # Find matching run2 snapshot
    run2_comp = None
    for si, sc in zip(snap_interactions, snap_compression):
        if si == inter:
            run2_comp = round(sc, 6)
            break
    comparison_table.append({
        "interaction": inter,
        "run1_compression": run1_data[inter],
        "run2_compression": run2_comp,
        "delta": round(run2_comp - run1_data[inter], 6) if run2_comp else None,
    })

# When did run2 first beat run1's minimum?
run2_first_beat_run1 = None
for inter, comp in zip(snap_interactions, snap_compression):
    if comp < run1_min_compression:
        run2_first_beat_run1 = inter
        break

comparison = {
    "note": "Run 1 used strict bracket matching (unbalanced = no execution). Run 2 uses graceful matching.",
    "run1_min_compression": run1_min_compression,
    "run1_max_interaction": 900_000,
    "run1_was_killed": True,
    "side_by_side": comparison_table,
    "run2_first_beat_run1_min_at": run2_first_beat_run1,
}

# ──────────────────────────────────────────────
# 6. RAW EXPERIMENT CONFIG
# ──────────────────────────────────────────────
print("Collecting experiment config...")

# Git hash
try:
    git_hash = subprocess.check_output(
        ["git", "rev-parse", "HEAD"],
        cwd="/Users/motherlabs/Desktop/mlabs",
        stderr=subprocess.DEVNULL,
    ).decode().strip()
except Exception:
    git_hash = "not a git repo"

experiment_config = {
    "config": config,
    "git_commit_hash": git_hash,
    "total_wall_clock_seconds": round(total_wall_clock, 1),
    "total_wall_clock_human": f"{total_wall_clock / 60:.1f} minutes",
    "python_version": sys.version,
    "bracket_matching": "graceful (unmatched brackets become no-ops)",
}

# ──────────────────────────────────────────────
# Assemble and save
# ──────────────────────────────────────────────
report = {
    "experiment": "BFF Abiogenesis Simulation — Run 2 (graceful brackets)",
    "generated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
    "phase_transition": phase_transition,
    "replicator_census": replicator_census,
    "symbiogenesis": symbiogenesis,
    "soup_snapshots": soup_snapshots,
    "comparison_to_run1": comparison,
    "experiment_config": experiment_config,
}

output_path = str(RUN_DIR / "results_summary.json")
with open(output_path, "w") as f:
    json.dump(report, f, indent=2)

print(f"\nReport saved to {output_path}")
print(f"Report size: {os.path.getsize(output_path) / 1024:.1f} KB")
