//! Conformance corpus for the state-feeds extension
//! (`feed-conformance/`). Replay-equivalence, reused wholesale from
//! interactive worlds: the same signed op log replays to the identical
//! state hash on every sink — hash equality, no tolerance. Sealed
//! vectors carry their checkpoint hashes; live vectors carry an
//! adversarial arrival order + the final hash every sink must reach.
//!
//!   wai_feed_vectors gen <dir>  |  verify <dir>

use std::path::Path;
use wai::world::feed::{add_payload, feed_mutation as m, set_cell, Cell, FeedRecord};
use wai::world::replay::{run_sealed_dispatch, Ingest, LiveWorld};
use wai::world::{
    mint_node_id, CausalOp, FeedState, Fx, SimContract, WorldContainer, WorldState, Xoshiro256pp,
    CAP_SIM_FEED0, NUMERIC_FIXED64, RESOLVE_LWW,
};
use ed25519_dalek::SigningKey;

const PUBLISHER_SEED: u8 = 9;

fn main() {
    let a: Vec<String> = std::env::args().collect();
    match (a.get(1).map(String::as_str), a.get(2)) {
        (Some("gen"), Some(d)) => generate(Path::new(d)),
        (Some("verify"), Some(d)) => verify(Path::new(d)),
        _ => {
            eprintln!("usage: wai_feed_vectors <gen|verify> <corpus-dir>");
            std::process::exit(2);
        }
    }
}

fn hex(b: &[u8]) -> String {
    b.iter().map(|x| format!("{x:02x}")).collect()
}
fn key(b: u8) -> SigningKey {
    SigningKey::from_bytes(&[b; 32])
}
fn fx(n: i32) -> Fx {
    Fx::from_int(n)
}
fn fxd(n: i32, d: i32) -> Fx {
    Fx::from_int(n) / Fx::from_int(d)
}
fn body(cells: &[Cell]) -> Vec<u8> {
    FeedRecord { cells: cells.to_vec() }.to_body()
}

/// A feed contract: tick_rate 1, per-tick checkpoints (a feed advances
/// one event-tick at a time; the identity step makes the rate cosmetic).
fn feed_contract() -> SimContract {
    SimContract {
        tick_rate: 1,
        numeric: NUMERIC_FIXED64.into(),
        rng_alg: "xoshiro256++".into(),
        rng_seed: [0u8; 32],
        sim: CAP_SIM_FEED0.into(),
        resolve_default: RESOLVE_LWW.into(),
        checkpoint_every: 1,
    }
}

fn scene() -> WorldContainer {
    let c = feed_contract();
    WorldContainer {
        contract_bytes: c.to_canonical_json(),
        contract: c,
        graph_bytes: WorldState::<FeedRecord>::new().canonical_bytes(&FeedState),
        assets: vec![],
        ops: vec![],
        seal: None,
    }
}

// ---------------------------------------------------------------------------
// Vector op sets
// ---------------------------------------------------------------------------

/// A scoreboard: two team records `{ name:Text, score:Int }`. Scores
/// rise by SUM increments (commutative), with one LWW correction.
fn ops_scoreboard() -> Vec<CausalOp> {
    let h = key(1);
    let v = key(2);
    let hn = mint_node_id(&h.verifying_key().to_bytes(), 1);
    let vn = mint_node_id(&v.verifying_key().to_bytes(), 1);
    vec![
        CausalOp::sign(&h, 1, 1, vec![], hn, m::CREATE, body(&[Cell::Text(b"HOME".to_vec()), Cell::Int(0)])),
        CausalOp::sign(&v, 1, 1, vec![], vn, m::CREATE, body(&[Cell::Text(b"AWAY".to_vec()), Cell::Int(0)])),
        // touchdown + extra point, field goal, etc. — order within a tick
        // cannot matter for SUM.
        CausalOp::sign(&h, 2, 2, vec![], hn, m::ADD_INT, add_payload(1, 7)),
        CausalOp::sign(&v, 2, 3, vec![], vn, m::ADD_INT, add_payload(1, 3)),
        CausalOp::sign(&h, 3, 4, vec![], hn, m::ADD_INT, add_payload(1, 3)),
        // a scorekeeper correction (LWW absolute set) at tick 5
        CausalOp::sign(&v, 3, 5, vec![], vn, m::SET_CELL, set_cell(1, &Cell::Int(6))),
        CausalOp::sign(&v, 4, 6, vec![], vn, m::ADD_INT, add_payload(1, 7)),
    ]
}

/// Telemetry: one sensor record `{ id:Text, temp:Fixed, packets:Int }`.
/// `temp` set absolutely (LWW), `packets` accumulated (SUM).
fn ops_telemetry() -> Vec<CausalOp> {
    let s = key(3);
    let n = mint_node_id(&s.verifying_key().to_bytes(), 1);
    vec![
        CausalOp::sign(&s, 1, 1, vec![], n, m::CREATE, body(&[Cell::Text(b"sensor-01".to_vec()), Cell::Fixed(fx(20)), Cell::Int(0)])),
        CausalOp::sign(&s, 2, 2, vec![], n, m::ADD_INT, add_payload(2, 128)),
        CausalOp::sign(&s, 3, 3, vec![], n, m::SET_CELL, set_cell(1, &Cell::Fixed(fxd(425, 10)))), // 42.5°
        CausalOp::sign(&s, 4, 4, vec![], n, m::ADD_FIXED, add_payload(1, fxd(-15, 10).to_bits())), // -1.5°
        CausalOp::sign(&s, 5, 5, vec![], n, m::ADD_INT, add_payload(2, 96)),
    ]
}

/// Market tape: one instrument `{ symbol:Text, last:Fixed, volume:Int }`.
/// `last` is the most recent print (LWW), `volume` accumulates (SUM).
fn ops_market() -> Vec<CausalOp> {
    let t = key(4);
    let n = mint_node_id(&t.verifying_key().to_bytes(), 1);
    vec![
        CausalOp::sign(&t, 1, 1, vec![], n, m::CREATE, body(&[Cell::Text(b"ACME".to_vec()), Cell::Fixed(fxd(1000, 10)), Cell::Int(0)])),
        CausalOp::sign(&t, 2, 2, vec![], n, m::SET_CELL, set_cell(1, &Cell::Fixed(fxd(1012, 10)))),
        CausalOp::sign(&t, 3, 2, vec![], n, m::ADD_INT, add_payload(2, 500)),
        CausalOp::sign(&t, 4, 3, vec![], n, m::SET_CELL, set_cell(1, &Cell::Fixed(fxd(1007, 10)))),
        CausalOp::sign(&t, 5, 3, vec![], n, m::ADD_INT, add_payload(2, 250)),
    ]
}

fn seal_from_live(mut c: WorldContainer, ops: &[CausalOp], final_tick: u64) -> WorldContainer {
    let mut live = LiveWorld::open(FeedState, c.clone()).expect("open");
    for op in ops {
        let outcome = live.ingest(op.clone()).expect("ingest");
        assert!(matches!(outcome, Ingest::Applied | Ingest::Buffered));
    }
    let final_hash = live.advance_to(final_tick).expect("advance");
    c.ops = live.canonical_ops();
    c.seal(&key(PUBLISHER_SEED), final_tick, final_hash);
    c
}

fn write_ops(path: &Path, ops: &[CausalOp]) {
    let bytes: Vec<u8> = ops.iter().flat_map(|o| o.to_bytes()).collect();
    std::fs::write(path, bytes).expect("write ops");
}

/// Seeded shuffles — the corpus has no entropy anywhere.
fn shuffles(ops: &[CausalOp], n: usize) -> Vec<Vec<CausalOp>> {
    let mut rng = Xoshiro256pp::from_seed([7u8; 32]);
    (0..n)
        .map(|_| {
            let mut s = ops.to_vec();
            for i in (1..s.len()).rev() {
                let j = (rng.next_u64() % (i as u64 + 1)) as usize;
                s.swap(i, j);
            }
            s
        })
        .collect()
}

// ---------------------------------------------------------------------------

fn generate(dir: &Path) {
    let vectors = dir.join("vectors");
    std::fs::create_dir_all(&vectors).expect("mkdir");
    let mut manifest = serde_json::Map::new();

    let sealed: Vec<(&str, WorldContainer)> = vec![
        ("scoreboard", seal_from_live(scene(), &ops_scoreboard(), 6)),
        ("telemetry", seal_from_live(scene(), &ops_telemetry(), 5)),
        ("market_ticks", seal_from_live(scene(), &ops_market(), 3)),
    ];
    for (name, c) in &sealed {
        let file = format!("{name}.wwld");
        std::fs::write(vectors.join(&file), c.to_bytes()).expect("write wwld");
        let cps = run_sealed_dispatch(c).expect("replay");
        manifest.insert(
            file,
            serde_json::json!({
                "type": "sealed",
                "sim": CAP_SIM_FEED0,
                "publisher": hex(&key(PUBLISHER_SEED).verifying_key().to_bytes()),
                "checkpoints": cps.iter()
                    .map(|cp| serde_json::json!({"tick": cp.tick, "hash": hex(&cp.hash)}))
                    .collect::<Vec<_>>(),
            }),
        );
    }

    // Live convergence: the scoreboard op stream delivered in adversarial
    // orders all reach the one canonical final hash.
    let scene_file = "scene.wwld";
    std::fs::write(vectors.join(scene_file), scene().to_bytes()).expect("write scene");
    for (i, s) in shuffles(&ops_scoreboard(), 3).into_iter().enumerate() {
        let name = format!("scoreboard_shuffled_{}", i + 1);
        let file = format!("{name}.ops");
        write_ops(&vectors.join(&file), &s);
        let mut live = LiveWorld::open(FeedState, scene()).expect("open");
        for op in &s {
            live.ingest(op.clone()).expect("ingest");
        }
        let h = live.advance_to(6).expect("advance");
        manifest.insert(
            file,
            serde_json::json!({ "type": "live", "scene": scene_file, "final_tick": 6, "final_hash": hex(&h) }),
        );
    }

    std::fs::write(
        dir.join("expected_hashes.json"),
        serde_json::to_string_pretty(&serde_json::Value::Object(manifest)).unwrap() + "\n",
    )
    .expect("manifest");
    println!("generated feed vectors into {}", dir.display());
}

fn verify(dir: &Path) {
    let m: serde_json::Value =
        serde_json::from_slice(&std::fs::read(dir.join("expected_hashes.json")).expect("manifest")).expect("parse");
    let vectors = dir.join("vectors");
    let mut fails = 0;
    for (file, spec) in m.as_object().expect("obj") {
        let kind = spec["type"].as_str().unwrap();
        let bytes = std::fs::read(vectors.join(file)).expect("vector file");
        let ok = match kind {
            "sealed" => {
                let c = WorldContainer::from_bytes(&bytes).expect("parse wwld");
                match run_sealed_dispatch(&c) {
                    Ok(cps) => spec["checkpoints"].as_array().unwrap().iter().zip(&cps).all(|(e, got)| {
                        e["tick"].as_u64() == Some(got.tick) && e["hash"].as_str() == Some(&hex(&got.hash))
                    }) && cps.len() == spec["checkpoints"].as_array().unwrap().len(),
                    Err(_) => false,
                }
            }
            "live" => {
                let scene_bytes = std::fs::read(vectors.join(spec["scene"].as_str().unwrap())).expect("scene");
                let c = WorldContainer::from_bytes(&scene_bytes).expect("parse scene");
                let ops = CausalOp::parse_log(&bytes).expect("ops");
                let mut live = LiveWorld::open(FeedState, c).expect("open");
                for op in ops {
                    live.ingest(op).expect("ingest");
                }
                let h = live.advance_to(spec["final_tick"].as_u64().unwrap()).expect("advance");
                hex(&h) == spec["final_hash"].as_str().unwrap()
            }
            other => {
                eprintln!("unknown vector type {other}");
                false
            }
        };
        println!("{} {file}", if ok { "PASS" } else { "FAIL" });
        if !ok {
            fails += 1;
        }
    }
    if fails > 0 {
        eprintln!("{fails} failed — NOT conformant");
        std::process::exit(1);
    }
    println!("all vectors pass: replay-equivalence holds for state-feeds");
}
