/**
 * ReplicatedPeerStreamReader — materializes a replicated store's per-origin records
 * from the coherence-journal streams on disk (WS2 send-side, the union-read half).
 *
 * Spec: docs/specs/WS2-SEND-SIDE-EMISSION-SPEC.md §3.3 + §3.4; the substrate is
 * docs/specs/multi-machine-replicated-store-foundation.md §7.1 (namespaced per-origin
 * storage), §7.2 (the union the reader merges).
 *
 * THE GAP THIS CLOSES: `ReplicatedStoreReader.loadOriginRecords` returned only the OWN
 * origin, so even a correctly-received peer record (durably written by
 * JournalSyncApplier to `peers/<M>.<kind>.jsonl`) was invisible to a read. This reader
 * is the seam that makes the union real: it reads the OWN stream
 * (`<self>.<kind>.jsonl` + archives) AND every peer replica stream
 * (`peers/<M>.<kind>.jsonl` + archives; quarantine + meta excluded), validates each
 * line through the SAME `validateReplicatedEnvelope` + store schema the writer used,
 * and folds to the LATEST record per `(origin, recordKey)` by HLC-max. A delete is a
 * tombstone (kept) so the union's delete-resolution + resurrection guard work.
 *
 * It supplies three seams to the rest of the system:
 *   - `loadOriginRecords(store, recordKey)` + `listRecordKeys(store)` — the
 *     ReplicatedStoreReader seams (the no-clobber union's per-origin input).
 *   - `loadWitness(store, recordKey)` — the emitter's `observed` source (the MAX HLC
 *     over every origin record held for the key — own prior + applied peers).
 *   - `loadOwnEntries(store, origin)` — the snapshot-serve seam (raw OWN entries by
 *     kind), replacing the `loadOwnEntries: () => ({})` stub.
 *
 * PURE-ish: all I/O is the injected `fsImpl` (defaults to node:fs); no Date, no
 * network. Bounded — replicated memory stores are small (per-kind retention caps), and
 * the read ceiling mirrors the applier's SERVE_READ_BYTE_CEILING.
 */
import fs from 'node:fs';
import path from 'node:path';
import { sanitizeMachineId, readTailTolerant, } from './CoherenceJournal.js';
import { HybridLogicalClock } from './HybridLogicalClock.js';
import { validateReplicatedEnvelope, } from './ReplicatedRecordEnvelope.js';
/** The byte ceiling for one stream read (mirrors JournalSyncApplier.SERVE_READ_BYTE_CEILING). */
const READ_BYTE_CEILING = 64 * 1024 * 1024;
/** A counters bag that ignores every bump — the reader surfaces nothing per-field
 *  (a malformed replica line is simply dropped from the union, the safe direction). */
const NOOP_COUNTERS = {
    bumpSchemaReject: () => { },
    bumpDroppedField: () => { },
    bumpJailReject: () => { },
};
function realFs() {
    return {
        openSync: fs.openSync,
        writeSync: fs.writeSync,
        fdatasyncSync: fs.fdatasyncSync,
        closeSync: fs.closeSync,
        existsSync: fs.existsSync,
        statSync: fs.statSync,
        renameSync: fs.renameSync,
        writeFileSync: fs.writeFileSync,
        readFileSync: fs.readFileSync,
        readdirSync: fs.readdirSync,
        truncateSync: fs.truncateSync,
        mkdirSync: fs.mkdirSync,
        readSync: fs.readSync,
    };
}
function escapeRegExp(s) {
    return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// RULE 3: EXEMPT — this is NOT a state-detector. ReplicatedPeerStreamReader reads
// the agent's OWN coherence-journal stream files (a fixed, self-authored JSONL format)
// and validates each line through validateReplicatedEnvelope. It does not parse
// provider/CLI output, does not detect external/environment state, and has no
// signature-matching that could drift across providers — it deterministically folds
// in-house journal entries by HLC. The "*Reader" name matches the Rule-3 pattern
// heuristic but the substance is an internal-format reader (mirrors the same exemption
// on ReplicatedStoreReader).
export class ReplicatedPeerStreamReader {
    stateDir;
    registry;
    selfMachineId;
    io;
    constructor(config) {
        if (!config)
            throw new Error('ReplicatedPeerStreamReader: config required');
        if (!config.registry)
            throw new Error('ReplicatedPeerStreamReader: registry required (not null)');
        if (typeof config.selfMachineId !== 'string' || config.selfMachineId.length === 0) {
            throw new Error('ReplicatedPeerStreamReader: selfMachineId must be a non-empty string');
        }
        this.stateDir = config.stateDir;
        this.registry = config.registry;
        this.selfMachineId = config.selfMachineId;
        this.io = config.fsImpl ?? realFs();
    }
    // ── The ReplicatedStoreReader seams ────────────────────────────────────────
    /**
     * Every origin's CURRENT record for (store, recordKey) — the own stream + each peer
     * replica namespace, ONE record per origin (the latest by HLC, a delete kept as a
     * tombstone). The union reader merges these via the no-clobber rule. A single-machine
     * install returns only the own origin (so the union is a strict no-op = that record).
     */
    loadOriginRecords(store, recordKey) {
        const byOrigin = this.materialize(store).get(recordKey);
        return byOrigin ? [...byOrigin.values()] : [];
    }
    /** Every recordKey the store currently holds across ALL origins (for readAll). */
    listRecordKeys(store) {
        return [...this.materialize(store).keys()];
    }
    // ── The emitter `observed`-witness seam (§7.2) ─────────────────────────────
    /**
     * The MAX HLC over every origin record this machine currently holds for
     * (store, recordKey) — own prior + applied peers. The author stamps this as the
     * record's `observed` witness. Returns undefined when none is held (first write of
     * the key) ⇒ the author omits `observed` ⇒ flag-on-conflict (the safe direction).
     * Sound: it only witnesses a version provably on disk, so a not-yet-pulled peer
     * version is absent here and the merge flags concurrent rather than silently
     * resolving (§7.2 err-toward-flag).
     */
    loadWitness(store, recordKey) {
        const records = this.loadOriginRecords(store, recordKey);
        let max;
        for (const r of records) {
            if (max === undefined || HybridLogicalClock.compare(r.envelope.hlc, max) > 0) {
                max = r.envelope.hlc;
            }
        }
        return max;
    }
    // ── The snapshot-serve seam (replaces loadOwnEntries: () => ({})) ──────────
    /**
     * The OWN-stream raw entries for `store`, keyed by contributing journal kind — the
     * single-origin snapshot input (§6). `origin` MUST be this machine (single-origin,
     * §6.1); a request for any other origin returns `{}` (we only serve what we
     * authored). Reads the own current + archive streams; returns ALL entries (the
     * materializer folds to the latest per recordKey and computes the seq watermark).
     */
    loadOwnEntries(store, origin) {
        const reg = this.registry.getByStore(store);
        if (!reg)
            return {};
        // Single-origin: only serve OUR OWN authored stream.
        if (origin !== this.selfMachineId)
            return {};
        const kind = reg.kind;
        const entries = this.readKindEntries(this.ownStreamFiles(origin, kind));
        if (entries.length === 0)
            return {};
        const raw = entries.map((e) => ({
            seq: e.seq,
            ts: e.ts,
            machine: e.machine,
            kind: e.kind,
            data: e.data,
        }));
        return { [kind]: raw };
    }
    // ── Materialization (own + peer streams → latest per (origin, recordKey)) ──
    /**
     * Build `recordKey → (origin → latest OriginRecord)` for a store by reading the own
     * stream + every peer replica stream and folding by HLC-max per (origin, recordKey).
     * Read FRESH each call (no cache) so a write-then-read is always consistent — the
     * stores are small + bounded, so the scan is cheap.
     */
    materialize(store) {
        const out = new Map();
        const reg = this.registry.getByStore(store);
        if (!reg)
            return out;
        const kind = reg.kind;
        const schema = reg.schema;
        const files = [
            ...this.ownStreamFiles(this.selfMachineId, kind),
            ...this.peerStreamFiles(kind),
        ];
        for (const file of files) {
            const entries = this.readKindEntries([file]);
            for (const entry of entries) {
                if (entry.kind !== kind)
                    continue;
                const data = entry.data;
                if (!data || typeof data !== 'object')
                    continue;
                const result = validateReplicatedEnvelope(data, schema, NOOP_COUNTERS);
                if (!result.ok)
                    continue;
                const origin = result.envelope.origin;
                const recordKey = result.envelope.recordKey;
                const rec = { origin, envelope: result.envelope, data: result.storeFields };
                let byOrigin = out.get(recordKey);
                if (!byOrigin) {
                    byOrigin = new Map();
                    out.set(recordKey, byOrigin);
                }
                const prior = byOrigin.get(origin);
                // Keep the LATEST record per (origin, recordKey) by HLC-max (a delete is a
                // tombstone — kept, so the union resolves delete↔put deterministically).
                if (!prior || HybridLogicalClock.compare(rec.envelope.hlc, prior.envelope.hlc) > 0) {
                    byOrigin.set(origin, rec);
                }
            }
        }
        return out;
    }
    /** Read + tolerant-parse every JournalEntry from the given stream files. */
    readKindEntries(files) {
        const out = [];
        for (const f of files) {
            const read = readTailTolerant(this.io, f, Number.MAX_SAFE_INTEGER, READ_BYTE_CEILING);
            for (const e of read.entries)
                out.push(e);
        }
        return out;
    }
    // ── Path enumeration ───────────────────────────────────────────────────────
    journalDir() {
        return path.join(this.stateDir, 'state', 'coherence-journal');
    }
    peersDir() {
        return path.join(this.journalDir(), 'peers');
    }
    /** Own current + archive stream files for (origin, kind), newest-first by stamp. */
    ownStreamFiles(origin, kind) {
        const safe = sanitizeMachineId(origin);
        const dir = this.journalDir();
        const current = path.join(dir, `${safe}.${kind}.jsonl`);
        const out = [];
        if (this.io.existsSync(current))
            out.push(current);
        out.push(...this.archivesFor(dir, safe, kind));
        return out;
    }
    /** Every peer replica current + archive stream file for `kind` (quarantine excluded). */
    peerStreamFiles(kind) {
        const dir = this.peersDir();
        let names;
        try {
            names = this.io.readdirSync(dir);
        }
        catch { /* @silent-fallback-ok: peers dir absent = no peers yet (single-machine / fresh) — an empty union, never an error. */
            return [];
        }
        const k = escapeRegExp(kind);
        // `<id>.<kind>.jsonl` (current) OR `<id>.<kind>.<digits>.jsonl` (archive). The
        // `<id>` is greedy (`.+`) and may itself contain dots. Quarantine files
        // (`<id>.<kind>.quarantine.<digits>.jsonl`) are EXCLUDED — they are a fenced
        // old incarnation, never part of the live union.
        const re = new RegExp(`^(.+)\\.${k}(?:\\.\\d+)?\\.jsonl$`);
        const out = [];
        for (const n of names) {
            if (n.includes('.quarantine.'))
                continue;
            if (re.test(n))
                out.push(path.join(dir, n));
        }
        return out;
    }
    /** Archive files `<safe>.<kind>.<stamp>.jsonl` in `dir`, newest-first. */
    archivesFor(dir, safe, kind) {
        let names;
        try {
            names = this.io.readdirSync(dir);
        }
        catch { /* @silent-fallback-ok: dir absent = nothing to read — empty, never an error. */
            return [];
        }
        const re = new RegExp(`^${escapeRegExp(`${safe}.${kind}.`)}(\\d+)\\.jsonl$`);
        const archives = [];
        for (const n of names) {
            const m = re.exec(n);
            if (m)
                archives.push({ file: path.join(dir, n), stamp: Number(m[1]) });
        }
        archives.sort((a, b) => b.stamp - a.stamp);
        return archives.map((a) => a.file);
    }
}
//# sourceMappingURL=ReplicatedPeerStreamReader.js.map