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"

# ───── 2b-4: a failed write or rename removes its temp file ─────
ed(JT, """    let kept = lines[lines.len() - keep..].join("\\n");
    let temp = temp_beside(path);
    std::fs::write(&temp, format!("{kept}\\n"))?;
    std::fs::rename(&temp, path)
}
""", """    let kept = lines[lines.len() - keep..].join("\\n");
    replace_via_temp(path, format!("{kept}\\n"))
}

/// Write `contents` to a fresh temp file beside `path`, then rename it over
/// `path`. A failure removes the temp file (best effort), so a unique temp
/// name never orphans a `*.trim.<pid>.<n>` (#346 2b-4).
fn replace_via_temp(path: &Path, contents: String) -> io::Result<()> {
    let temp = temp_beside(path);
    let result = std::fs::write(&temp, contents).and_then(|()| std::fs::rename(&temp, path));
    if result.is_err() {
        let _ = std::fs::remove_file(&temp);
    }
    result
}
""")
ed(JT, """        out.push('\\n');
    }
    let temp = temp_beside(path);
    std::fs::write(&temp, out)?;
    std::fs::rename(&temp, path)
}
""", """        out.push('\\n');
    }
    replace_via_temp(path, out)
}
""")

# ───── 2b-3: receipt_session shape ─────
ed(TW, """        peer_ok
            && (crate::msgid::is_short_id(&self.msg_id) || is_op_ref(&self.msg_id))
            && is_op_ref(&self.claim_ref)
    }
""", """        peer_ok
            && (crate::msgid::is_short_id(&self.msg_id) || is_op_ref(&self.msg_id))
            && is_op_ref(&self.claim_ref)
            && self.receipt_session.as_deref().is_none_or(is_session_id_shape)
    }
""")
ed(TW, """/// Whether `s` has [`op_ref`]'s shape.""", """/// Whether `s` has a session id's shape, so [`WarningRecord::claim_key`] is a
/// safe path component (#346 2b-3): 1..=128 characters from `[A-Za-z0-9._-]`,
/// and not made of dots alone. `perch::session_dir` maps `/`, `\\` and `:`
/// but not a bare `..`, which would name the parent of the sessions root.
fn is_session_id_shape(s: &str) -> bool {
    (1..=128).contains(&s.len())
        && s.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
        && !s.bytes().all(|b| b == b'.')
}

/// Whether `s` has [`op_ref`]'s shape.""")

# ───── 2b-2: malformed rows cannot hide fullness from R2-1 ─────
ed(TW, """pub fn record_warning_at(perch_path: &Path, record: &WarningRecord) -> io::Result<bool> {
    let mut records = read_records_at(perch_path);
""", """pub fn record_warning_at(perch_path: &Path, record: &WarningRecord) -> io::Result<bool> {
    let file = records_file_at(perch_path);
    let mut records = read_records_at(perch_path);
    // A line the reader dropped (malformed, or torn) still occupies the file,
    // and `append_at`'s trim counts LINES: with k such lines a full file reads
    // as k short of full, the per-peer eviction below is skipped, and the trim
    // drops the oldest line, which can be another peer's pending record
    // (#346 2b-2). So the file is first rewritten to exactly the rows read.
    let raw_lines = std::fs::read_to_string(&file)
        .map(|s| s.lines().filter(|l| !l.trim().is_empty()).count())
        .unwrap_or(0);
    if raw_lines != records.len() {
        crate::jsonltail::rewrite_at(&file, &records)?;
    }
""")

# ───── 2b-3 assert in the malformed-row unit ─────
ed(NS, """        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] {
""", """        let mut no_ref = spt_store::trustwarn::WarningRecord::new(&stranger(), Some(M3), "op-3", None, "x", 1);
        no_ref.claim_ref = String::new();
        // #346 2b-3: a receipt session that is not a session id's shape would
        // be a path component; `..` is the one `session_dir` does not map.
        let bad_session = spt_store::trustwarn::WarningRecord::new(&stranger(), None, "op-4", Some(".."), "x", 1);
        for row in [&bad_id, &bad_peer, &no_ref, &bad_session, &good] {
""")

tail(NS, """
    // [unit->REQ-TRUST-WARNING-CADENCE] doyle gate W5 2b-2: rows the reader
    // drops still occupy LINES, and the file's trim counts lines. Bravo's one
    // pending record is on line 1, a malformed row on line 2, and a stranger
    // then floods: bravo's record survives. Red-on-purpose: leaving the
    // malformed line in place makes the file read one short of full, skips the
    // per-peer eviction, and trims bravo.
    #[test]
    fn a_malformed_line_does_not_switch_off_the_per_peer_eviction() {
        let _home = crate::testutil::isolated_home();
        let perch_path = perch::resolve_perch_path("tw-flood-bad", perch::ParentHint::Infer);
        std::fs::create_dir_all(&perch_path).unwrap();
        let file = spt_store::trustwarn::records_file_at(&perch_path);
        let b = spt_store::trustwarn::WarnedPeer::Endpoint("bravo".to_string());
        let bravo = spt_store::trustwarn::WarningRecord::new(&b, Some(M1), "op-bravo", None, "bravo's only warning", 1);
        spt_store::jsonltail::append_at(&file, &bravo, 256).unwrap();
        let mut bad = spt_store::trustwarn::WarningRecord::new(&stranger(), Some(M2), "op-bad", None, "x", 1);
        bad.peer = "a</TRUST_WARNINGS>".to_string();
        spt_store::jsonltail::append_at(&file, &bad, 256).unwrap();
        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] doyle gate W5 2b-5: the `node` arm
    // of the well-formed check is proven against a REAL node id. The WAN
    // origin is the peer's `PublicKey::to_hex` (64 lowercase hex characters),
    // so an UnnamedOn record built from one reads back and renders.
    // Red-on-purpose: an arm narrower than hex drops every UnnamedOn warning.
    #[test]
    fn an_unnamed_peer_with_a_real_node_id_reads_back() {
        let _home = crate::testutil::isolated_home();
        let session = session_id("tw-node");
        let perch_path = perch::resolve_perch_path("tw-node", perch::ParentHint::Infer);
        std::fs::create_dir_all(&perch_path).unwrap();
        let node = spt_proto::identity::Identity::generate().public_key().to_hex();
        assert_eq!(node.len(), 64, "precondition: a real node id: {node}");
        let peer = spt_store::trustwarn::WarnedPeer::UnnamedOn(node.clone());
        let rec = spt_store::trustwarn::WarningRecord::new(&peer, Some(M1), "op-node", Some(&session), "caution", 1);
        assert!(spt_store::trustwarn::record_warning_at(&perch_path, &rec).unwrap());
        let rows = read_trust_rows(&perch_path);
        assert_eq!(rows.len(), 1, "the record reads back");
        assert_eq!(rows[0].peer, node);
        let mut seen = SeenSet::load(&session, Category::TrustWarnings);
        let lines = gather_trust_warnings(&poll_input("tw-node", &session, ""), &mut seen);
        assert_eq!(lines.len(), 1, "and renders: {lines:?}");
        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):
                print(f"FAIL {path}: unexpected end {s[-20:]!r}"); ok = False; continue
            s = s[: -len(end)] + 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'))
