import sys
ROOT = r"C:/Users/decid/Documents/projects/spt-core/.worktrees/351-w5/"
edits = {}
def ed(path, old, new, count=1):
    edits.setdefault(path, []).append(("lit", old, new, count))
def tail(path, new):
    edits.setdefault(path, []).append(("tail", None, new, 1))

TW = "crates/spt-store/src/trustwarn.rs"
JT = "crates/spt-store/src/jsonltail.rs"
NS = "crates/spt/src/api/nowsignal.rs"

# ───────────── trustwarn.rs ─────────────
ed(TW, """    /// receiver-derived `op-<digest>`. Never raw sender bytes (#346 H1).
    pub msg_id: String,
""", """    /// receiver-derived `op-<digest>`. Never raw sender bytes (#346 H1).
    pub msg_id: String,
    /// A reference unique to THIS received message and wholly the receiver's:
    /// `op-<digest of the op id>` (#346 R2-3). The unbound claim key and the
    /// seen key are built from it, never from `msg_id`, which a sender can
    /// repeat. A row without one is malformed and is never read.
    #[serde(default)]
    pub claim_ref: String,
""")
ed(TW, """pub fn message_ref(msg_id_attr: Option<&str>, op_id: &str) -> String {
    if let Some(id) = msg_id_attr.filter(|id| crate::msgid::is_short_id(id)) {
        return id.to_string();
    }
    use sha2::{Digest, Sha256};
    let digest: [u8; 32] = Sha256::digest(op_id.as_bytes()).into();
    format!("op-{}", crate::msgid::short_id_of(&digest, OP_REF_LEN))
}
""", """pub fn message_ref(msg_id_attr: Option<&str>, op_id: &str) -> String {
    if let Some(id) = msg_id_attr.filter(|id| crate::msgid::is_short_id(id)) {
        return id.to_string();
    }
    op_ref(op_id)
}

/// `op-` plus a base32 digest of `op_id`: a receiver-derived reference that
/// carries no sender byte and is a legal path component everywhere.
fn op_ref(op_id: &str) -> String {
    use sha2::{Digest, Sha256};
    let digest: [u8; 32] = Sha256::digest(op_id.as_bytes()).into();
    format!("op-{}", crate::msgid::short_id_of(&digest, OP_REF_LEN))
}

/// Whether `s` has [`op_ref`]'s shape.
fn is_op_ref(s: &str) -> bool {
    s.strip_prefix("op-")
        .is_some_and(|rest| rest.len() == OP_REF_LEN && crate::msgid::is_short_id(rest))
}
""")
ed(TW, """        WarningRecord {
            msg_id: message_ref(msg_id_attr, op_id),
            peer_kind: peer_kind.to_string(),
""", """        WarningRecord {
            msg_id: message_ref(msg_id_attr, op_id),
            claim_ref: op_ref(op_id),
            peer_kind: peer_kind.to_string(),
""")
ed(TW, """    /// This record's key in the now-signal seen-set (#346 M2): the peer AND
    /// the message, so one message id reused across peers, or by a peer, never
    /// hides another record.
    pub fn seen_key(&self) -> String {
        format!("trust:{}:{}:{}", self.peer_kind, self.peer, self.msg_id)
    }

    /// The session the render claims ON THE RECORD'S BEHALF, beside the
    /// polling session: the one bound at receipt. A record received with no
    /// bound session gets a key of its own (`unbound-<msg-id>`), so rendering
    /// it once still retires it, while every such message stays its own
    /// warning (REQ-TRUST-WARNING-CADENCE (e): no key warns every message).
    pub fn claim_key(&self) -> String {
        self.receipt_session
            .clone()
            .unwrap_or_else(|| format!("unbound-{}", self.msg_id))
    }
""", """    /// This record's key in the now-signal seen-set (#346 M2, R2-3): the peer
    /// AND this received message, so a message id reused across peers, or by
    /// one peer, never hides another record.
    pub fn seen_key(&self) -> String {
        format!("trust:{}:{}:{}", self.peer_kind, self.peer, self.claim_ref)
    }

    /// The session the render claims ON THE RECORD'S BEHALF, beside the
    /// polling session: the one bound at receipt. A record received with no
    /// bound session gets a key of its own (`unbound-<claim-ref>`), so
    /// rendering it once still retires it, while every such message stays its
    /// own warning (REQ-TRUST-WARNING-CADENCE (e): no key warns every
    /// message). Keyed on the receiver's per-message reference, never on the
    /// `msg-id` a sender can repeat (#346 R2-3).
    pub fn claim_key(&self) -> String {
        self.receipt_session
            .clone()
            .unwrap_or_else(|| format!("unbound-{}", self.claim_ref))
    }

    /// Whether every sender-reachable field of a row read off disk has the
    /// shape [`WarningRecord::new`] would have given it (#346 R2-2). A row never
    /// passes through `new` when it is read back, so the reader checks it
    /// again: the KH line-safe class, at the lowest rung.
    // [impl->REQ-HAZARD-ENVELOPE-ATTR-LINESAFE]
    pub fn is_well_formed(&self) -> bool {
        let peer_ok = match self.peer_kind.as_str() {
            "endpoint" => validate_endpoint_id(&self.peer).is_ok(),
            "node" => !self.peer.is_empty() && self.peer.bytes().all(|b| b.is_ascii_alphanumeric()),
            _ => false,
        };
        peer_ok
            && (crate::msgid::is_short_id(&self.msg_id) || is_op_ref(&self.msg_id))
            && is_op_ref(&self.claim_ref)
    }
""")
ed(TW, """/// Every kept record, oldest first. The WHOLE kept file, never a shorter tail
/// (#346 M3): a pending record older than the tail would fall out of view
/// while still owed.
// [impl->REQ-TRUST-WARNING-NOW-SIGNAL]
pub fn read_records_at(perch_path: &Path) -> Vec<WarningRecord> {
    crate::jsonltail::read_at(&records_file_at(perch_path), RECORD_KEEP)
}
""", """/// Every kept, well-formed record, oldest first. The WHOLE kept file, never a
/// shorter tail (#346 M3): a pending record older than the tail would fall
/// out of view while still owed. A malformed row is dropped
/// ([`WarningRecord::is_well_formed`], #346 R2-2).
// [impl->REQ-TRUST-WARNING-NOW-SIGNAL]
pub fn read_records_at(perch_path: &Path) -> Vec<WarningRecord> {
    crate::jsonltail::read_at::<WarningRecord>(&records_file_at(perch_path), RECORD_KEEP)
        .into_iter()
        .filter(WarningRecord::is_well_formed)
        .collect()
}
""")
ed(TW, """    // A FULL file makes room by retiring RENDERED records first, oldest first
    // (#346 M3): a pending warning is never evicted while a rendered one
    // remains. Only a file that is all pending loses its oldest, which the
    // keep bound makes unavoidable.
    if records.len() >= RECORD_KEEP {
        let mut excess = records.len() + 1 - RECORD_KEEP;
        let before = records.len();
        records.retain(|r| {
            if excess > 0 && r.rendered() {
                excess -= 1;
                false
            } else {
                true
            }
        });
        if records.len() != before {
            crate::jsonltail::rewrite_at(&records_file_at(perch_path), &records)?;
        }
    }
""", """    // A FULL file makes room by retiring RENDERED records first, oldest first
    // (#346 M3): a pending warning is never evicted while a rendered one
    // remains.
    if records.len() >= RECORD_KEEP {
        let before = records.len();
        let mut excess = records.len() + 1 - RECORD_KEEP;
        records.retain(|r| {
            if excess > 0 && r.rendered() {
                excess -= 1;
                false
            } else {
                true
            }
        });
        // Still full, so everything left is pending: the OLDEST record of the
        // peer holding the MOST records goes, the incoming one counted (#346
        // R2-1). A flood from one peer therefore pays for itself, and a peer's
        // last pending record goes only once every peer is down to one.
        while records.len() >= RECORD_KEEP {
            let peer_of = |r: &WarningRecord| (r.peer_kind.clone(), r.peer.clone());
            let mut counts: std::collections::HashMap<(String, String), usize> =
                std::collections::HashMap::new();
            for r in records.iter().chain(std::iter::once(record)) {
                *counts.entry(peer_of(r)).or_default() += 1;
            }
            let most = counts.values().copied().max().unwrap_or(0);
            let Some(victim) = records.iter().position(|r| counts[&peer_of(r)] == most) else {
                break;
            };
            records.remove(victim);
        }
        if records.len() != before {
            crate::jsonltail::rewrite_at(&records_file_at(perch_path), &records)?;
        }
    }
""")

# ───────────── jsonltail.rs: unique temp names (the #359 one-liner) ─────────────
ed(JT, """/// Keep the newest `keep` records.""", """/// A temp path beside `path` that no concurrent caller shares: two writers,
/// in one process or two, never rename each other's half-written file
/// (releases#359).
fn temp_beside(path: &Path) -> std::path::PathBuf {
    static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
    let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
    path.with_extension(format!("jsonl.trim.{}.{n}", std::process::id()))
}

/// Keep the newest `keep` records.""")
ed(JT, """    let temp = path.with_extension("jsonl.trim");""", """    let temp = temp_beside(path);""", count=2)

# ───────────── nowsignal.rs: extract poll_blocks (R2-4) ─────────────
ed(NS, """pub fn cmd_now_signal(
    ctx: &Ctx,
    id: &str,
    session: &str,
    user_input: &str,
    agent_output: &str,
    spec: &NowSpec,
) -> i32 {
    let input = PollInput {
        id,
        session,
        user_input,
        agent_output,
    };
    let now = now_ms();

""", """pub fn cmd_now_signal(
    ctx: &Ctx,
    id: &str,
    session: &str,
    user_input: &str,
    agent_output: &str,
    spec: &NowSpec,
) -> i32 {
    let input = PollInput {
        id,
        session,
        user_input,
        agent_output,
    };
    let blocks = poll_blocks(ctx, &input, now_ms(), spec);
    if let Some(rendered) = compose(&blocks) {
        println!("{rendered}");
    }
    0
}

/// Every category's block for one poll under `spec`, in render order: the
/// verb's whole wiring short of printing, so a test drives exactly what the
/// verb runs (#346 R2-4).
// [impl->REQ-NOW-SIGNAL-VERB]
// [impl->REQ-NOW-SIGNAL-DELTA]
pub fn poll_blocks(ctx: &Ctx, input: &PollInput, now: u64, spec: &NowSpec) -> Vec<Block> {
    let session = input.session;
""")
ed(NS, """        let lines = match cat {
            Category::Hints => gather_hints(ctx, &input, spec.cap_for(cat)),
            Category::EndpointMentions => gather_endpoint_mentions(&input, &rows, &mut seen),
            Category::Monics => gather_monics(&input, &mut seen),
            Category::Shells => gather_shells(&input, &mut seen),
            Category::LastMsgs => gather_last_msgs(&input, now, &mut seen),
            Category::EdgeTransitions => gather_edge_transitions(&rows, &mut seen),
            Category::DispatchResults => gather_dispatch_results(&input, &mut seen),
            Category::Updates => gather_updates(ctx, &input, &mut seen),
            Category::SealBrief => gather_seal_brief(&mut seen),
            Category::FileAccessHelper => gather_file_access_helper(&input, &mut seen),
            Category::LanExposed => gather_lan_exposed(),
            Category::TrustWarnings => gather_trust_warnings(&input, &mut seen),
        };""", """        let lines = match cat {
            Category::Hints => gather_hints(ctx, input, spec.cap_for(cat)),
            Category::EndpointMentions => gather_endpoint_mentions(input, &rows, &mut seen),
            Category::Monics => gather_monics(input, &mut seen),
            Category::Shells => gather_shells(input, &mut seen),
            Category::LastMsgs => gather_last_msgs(input, now, &mut seen),
            Category::EdgeTransitions => gather_edge_transitions(&rows, &mut seen),
            Category::DispatchResults => gather_dispatch_results(input, &mut seen),
            Category::Updates => gather_updates(ctx, input, &mut seen),
            Category::SealBrief => gather_seal_brief(&mut seen),
            Category::FileAccessHelper => gather_file_access_helper(input, &mut seen),
            Category::LanExposed => gather_lan_exposed(),
            Category::TrustWarnings => gather_trust_warnings(input, &mut seen),
        };""")
ed(NS, """            blocks.push(Block {
                category: cat,
                lines,
            });
        }
    }

    if let Some(rendered) = compose(&blocks) {
        println!("{rendered}");
    }
    0
}
""", """            blocks.push(Block {
                category: cat,
                lines,
            });
        }
    }
    blocks
}
""")

# the unreachable seen-vs-cap arm (doyle: drop it or leave it) — make it explicit
ed(NS, """        let newest = group.last().expect("a group is never empty");
        if !seen.take_new(&newest.seen_key()) {
            continue;
        }
""", """        let newest = group.last().expect("a group is never empty");
        // Seen keys were filtered above and the room was just checked, so
        // this takes the key.
        seen.take_new(&newest.seen_key());
""")

tail(NS, """
    fn read_trust_rows(perch_path: &Path) -> Vec<spt_store::trustwarn::WarningRecord> {
        spt_store::trustwarn::read_records_at(perch_path)
    }

    // [unit->REQ-TRUST-WARNING-CADENCE] [unit->REQ-TRUST-WARNING-NOW-SIGNAL]
    // doyle gate W5 R2-1: a flood of UNBOUND records from one stranger pays for
    // itself. Peer B's one pending record survives 300 of A's, and the file
    // stays bounded. Red-on-purpose: trimming the oldest line evicts B.
    #[test]
    fn an_unbound_flood_never_evicts_another_peers_pending_warning() {
        let _home = crate::testutil::isolated_home();
        let perch_path = perch::resolve_perch_path("tw-flood", perch::ParentHint::Infer);
        std::fs::create_dir_all(&perch_path).unwrap();
        let b = spt_store::trustwarn::WarnedPeer::Endpoint("bravo".to_string());
        record_for(&perch_path, &b, M1, None, "bravo's only warning");
        for n in 0..300 {
            let rec = spt_store::trustwarn::WarningRecord::new(&stranger(), None, &format!("flood-{n}"), None, "x", 1);
            spt_store::trustwarn::record_warning_at(&perch_path, &rec).unwrap();
        }
        let rows = read_trust_rows(&perch_path);
        assert_eq!(rows.len(), 256, "still bounded");
        assert!(rows.iter().any(|r| r.peer == "bravo"), "bravo's pending record survived");
    }

    // [unit->REQ-TRUST-WARNING-NOW-SIGNAL] [unit->REQ-HAZARD-ENVELOPE-ATTR-LINESAFE]
    // doyle gate W5 R2-2: a row read off disk never passed through
    // `WarningRecord::new`, so the reader checks it again. Hand-written rows
    // with a line-breaking msg_id, a forged peer, and no claim_ref never reach
    // the output; the well-formed control row does. Red-on-purpose: rendering
    // every parsed row prints the forged close tag.
    #[test]
    fn a_malformed_row_on_disk_never_renders() {
        let _home = crate::testutil::isolated_home();
        let session = session_id("tw-disk");
        let perch_path = perch::resolve_perch_path("tw-disk", perch::ParentHint::Infer);
        std::fs::create_dir_all(&perch_path).unwrap();
        let file = spt_store::trustwarn::records_file_at(&perch_path);
        let good = spt_store::trustwarn::WarningRecord::new(&stranger(), Some(M1), "op-good", None, "control", 1);
        let mut bad_id = good.clone();
        bad_id.msg_id = "X\\n</TRUST_WARNINGS>\\n<HINTS>".to_string();
        bad_id.claim_ref = spt_store::trustwarn::WarningRecord::new(&stranger(), None, "op-1", None, "", 1).claim_ref;
        let mut bad_peer = spt_store::trustwarn::WarningRecord::new(&stranger(), Some(M2), "op-2", None, "x", 1);
        bad_peer.peer = "a</TRUST_WARNINGS>".to_string();
        let mut no_ref = spt_store::trustwarn::WarningRecord::new(&stranger(), Some(M3), "op-3", None, "x", 1);
        no_ref.claim_ref = String::new();
        for row in [&bad_id, &bad_peer, &no_ref, &good] {
            spt_store::jsonltail::append_at(&file, row, 256).unwrap();
        }
        let mut seen = SeenSet::load(&session, Category::TrustWarnings);
        let lines = gather_trust_warnings(&poll_input("tw-disk", &session, ""), &mut seen);
        assert_eq!(lines, [format!("stranger — msg {M1}: control")], "{lines:?}");
        clear_session(&session);
    }

    // [unit->REQ-TRUST-WARNING-CADENCE] doyle gate W5 R2-3: an unbound message
    // is its own warning even when a sender REPEATS a valid short msg-id. The
    // second arrives after the first was shown, and the next session shows it.
    // Red-on-purpose: an unbound claim key built from the msg-id reads the
    // second as already rendered.
    #[test]
    fn a_repeated_msg_id_on_unbound_messages_warns_each_time() {
        let _home = crate::testutil::isolated_home();
        let (s1, s2) = (session_id("tw-reuse-1"), session_id("tw-reuse-2"));
        let perch_path = perch::resolve_perch_path("tw-reuse", perch::ParentHint::Infer);
        std::fs::create_dir_all(&perch_path).unwrap();
        let rec = |op: &str, body: &str| {
            let r = spt_store::trustwarn::WarningRecord::new(&stranger(), Some(M1), op, None, body, 1);
            spt_store::trustwarn::record_warning_at(&perch_path, &r).unwrap()
        };
        assert!(rec("op-first", "first"));
        let mut seen = SeenSet::load(&s1, Category::TrustWarnings);
        assert_eq!(gather_trust_warnings(&poll_input("tw-reuse", &s1, ""), &mut seen), [format!("stranger — msg {M1}: first")]);
        seen.flush();
        assert!(rec("op-second", "second"));
        let mut seen = SeenSet::load(&s2, Category::TrustWarnings);
        assert_eq!(
            gather_trust_warnings(&poll_input("tw-reuse", &s2, ""), &mut seen),
            [format!("stranger — msg {M1}: second")],
            "the second message warns too"
        );
        clear_session(&s1);
        clear_session(&s2);
    }

    // [unit->REQ-TRUST-WARNING-NOW-SIGNAL] doyle gate W5 R2-4: M1 through the
    // WIRING. The verb's own block loop (`poll_blocks`, which `cmd_now_signal`
    // prints) with `max_lines: 0` still renders TRUST_WARNINGS, while the
    // budget still binds the other categories. Red-on-purpose: capping the
    // seen-set with the raw `max_lines` withholds the warning.
    #[test]
    fn a_zero_line_budget_still_renders_trust_warnings_through_the_poll() {
        let _home = crate::testutil::isolated_home();
        let session = session_id("tw-wired");
        let perch_path = perch::resolve_perch_path("tw-wired", perch::ParentHint::Infer);
        std::fs::create_dir_all(&perch_path).unwrap();
        record_for(&perch_path, &stranger(), M1, Some(&session), "caution");
        let spec = NowSpec::from_json(r#"{"only": ["SHELLS"], "max_lines": 0}"#);
        let ctx = Ctx { adapter: None, manifest: None, install_dir: None };
        let blocks = poll_blocks(&ctx, &poll_input("tw-wired", &session, ""), 1, &spec);
        let trust: Vec<&Block> = blocks.iter().filter(|b| b.category == Category::TrustWarnings).collect();
        assert_eq!(trust.len(), 1, "the category renders under max_lines 0");
        assert_eq!(trust[0].lines, [format!("stranger — msg {M1}: caution")]);
        assert!(
            blocks.iter().all(|b| b.category == Category::TrustWarnings),
            "control: the budget of 0 still binds every other category"
        );
        clear_session(&session);
    }
}
""")

ok = True
out = {}
for path, lst in edits.items():
    full = ROOT + path
    raw = open(full, 'rb').read()
    crlf = b'\r\n' in raw
    s = raw.decode('utf-8')
    conv = (lambda t: t.replace('\n', '\r\n')) if crlf else (lambda t: t)
    for kind, a, b, count in lst:
        if kind == "lit":
            o, n = conv(a), conv(b)
            c = s.count(o)
            if c != count:
                print(f"FAIL {path}: expected {count} got {c}: {a[:80]!r}"); ok = False; continue
            s = s.replace(o, n)
        elif kind == "tail":
            nl = '\r\n' if crlf else '\n'
            end = nl + "}" + nl
            if not s.endswith(end) and not s.endswith(nl + "}"):
                print(f"FAIL {path}: unexpected file end {s[-20:]!r}"); ok = False; continue
            base = s[: -len(end)] if s.endswith(end) else s[: -len(nl + "}")]
            s = base + nl + conv(b.lstrip('\n'))
    out[full] = s.encode('utf-8')
if not ok:
    print("NOTHING WRITTEN"); sys.exit(1)
for full, b in out.items():
    open(full, 'wb').write(b)
    print("wrote", full, b.count(b'\r\n'), b.count(b'\n'))
