/**
 * DiskArchiveBackend — reference filesystem-backed {@link ArchiveBackend}.
 *
 * Convention #8 (atomic writes): every file lands via write-tmp-rename; the
 * final `archive.json` marker is written LAST so a crash mid-bundle leaves
 * the directory visible but UN-marked, and `restore` treats such a bundle
 * as missing. This mirrors the Phase 5 materializer's "marker-last"
 * invariant.
 *
 * Layout:
 *
 *   {rootDir}/archive/{arc_<opaque>}/
 *     subsession.json        # Identity + metadata
 *     summary.json            # SessionSummaryRef (optional — present iff input.summaryRef)
 *     messages.jsonl          # One SessionMessage per line (append-style, serialized once)
 *     workspace.json          # WorkspaceRef (optional — present iff input.workspace)
 *     archive.json            # Marker — final write; presence = bundle committed
 *
 * Workspace directory contents are NOT archived — we persist the
 * {@link WorkspaceRef} only. The pattern doc §12.3 explicitly allows this:
 * the ref is the re-hydration handle; the worktree itself is disposed at
 * archive time via the caller's {@link WorkspaceBackendDriver}, not copied
 * into the archive bundle. This is a Phase 8 interpretation — see the
 * roadmap deliverable report for rationale (tar bundling deferred).
 *
 * See session-hierarchy.md §12.3.
 */
import { randomBytes } from 'node:crypto';
import { mkdir, readFile, readdir, rename, unlink, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
/**
 * Raised when {@link DiskArchiveBackend.restore} cannot resolve the supplied
 * ref — either the directory is missing, or the `archive.json` marker is
 * absent (crash-mid-write signal).
 */
export class ArchiveNotFoundError extends Error {
    details;
    constructor(details) {
        super(`Archive ${details.archiveRef} could not be resolved: ${details.reason}`);
        this.name = 'ArchiveNotFoundError';
        this.details = details;
    }
}
export class DiskArchiveBackend {
    kind = 'disk';
    rootDir;
    constructor(config) {
        this.rootDir = config.rootDir;
    }
    async store(input) {
        const archiveRef = generateArchiveBackendRef();
        const archivedAt = new Date();
        const dir = join(this.rootDir, 'archive', archiveRef);
        await mkdir(dir, { recursive: true });
        // 1. Sub-session identity.
        const subEntry = {
            subSessionId: input.subSessionId,
            sessionId: input.sessionId,
            tenantId: input.tenantId,
        };
        await atomicWriteJson(join(dir, 'subsession.json'), subEntry);
        // 2. Summary (optional).
        if (input.summaryRef) {
            const entry = { summary: serializeSummaryFields(input.summaryRef) };
            await atomicWriteJson(join(dir, 'summary.json'), entry);
        }
        // 3. Workspace ref (optional).
        if (input.workspace) {
            const entry = {
                workspace: serializeWorkspaceFields(input.workspace),
            };
            await atomicWriteJson(join(dir, 'workspace.json'), entry);
        }
        // 4. Messages — one JSON per line, serialized once with an atomic
        //    tmp-rename. No append I/O: we have the full bundle upfront, and
        //    single-write is atomic at the file level.
        const lines = input.messages.map((m) => {
            const line = {
                id: m.id,
                sessionId: m.sessionId,
                tenantId: m.tenantId,
                message: m.message,
                at: m.at.toISOString(),
            };
            return JSON.stringify(line);
        });
        const body = lines.length > 0 ? `${lines.join('\n')}\n` : '';
        await atomicWriteText(join(dir, 'messages.jsonl'), body);
        // 5. Marker — ALWAYS last. Presence = bundle committed.
        const marker = {
            archiveRef,
            archivedAt: archivedAt.toISOString(),
        };
        await atomicWriteJson(join(dir, 'archive.json'), marker);
        return { archiveRef, archivedAt };
    }
    async restore(archiveRef) {
        const dir = join(this.rootDir, 'archive', archiveRef);
        // Directory must exist.
        try {
            await readdir(dir);
        }
        catch (err) {
            const code = err.code;
            if (code === 'ENOENT') {
                throw new ArchiveNotFoundError({ archiveRef, reason: 'missing' });
            }
            throw err;
        }
        // Marker must exist — absence = crash-mid-write.
        const marker = await readJson(join(dir, 'archive.json'));
        if (!marker) {
            throw new ArchiveNotFoundError({ archiveRef, reason: 'incomplete' });
        }
        const subEntry = await readJson(join(dir, 'subsession.json'));
        if (!subEntry) {
            throw new ArchiveNotFoundError({ archiveRef, reason: 'incomplete' });
        }
        const summaryEntry = await readJson(join(dir, 'summary.json'));
        const summaryRef = summaryEntry ? deserializeSummaryFields(summaryEntry.summary) : undefined;
        const workspaceEntry = await readJson(join(dir, 'workspace.json'));
        const workspace = workspaceEntry
            ? deserializeWorkspaceFields(workspaceEntry.workspace)
            : undefined;
        const messagesRaw = await readText(join(dir, 'messages.jsonl'));
        const messages = messagesRaw === null
            ? []
            : messagesRaw
                .split('\n')
                .filter((l) => l.length > 0)
                .map((l) => {
                const raw = JSON.parse(l);
                return {
                    id: raw.id,
                    sessionId: raw.sessionId,
                    tenantId: raw.tenantId,
                    message: raw.message,
                    at: new Date(raw.at),
                };
            });
        return {
            subSessionId: subEntry.subSessionId,
            sessionId: subEntry.sessionId,
            tenantId: subEntry.tenantId,
            ...(workspace !== undefined && { workspace }),
            ...(summaryRef !== undefined && { summaryRef }),
            messages,
        };
    }
}
// Serialization helpers ------------------------------------------------------
function serializeSummaryFields(s) {
    // Dates survive JSON round-trip as strings; we let JSON.stringify handle
    // the normal `toJSON` path but store them as ISO strings on the wire.
    // On deserialize we coerce back. The in-memory record stays immutable.
    return s;
}
function deserializeSummaryFields(s) {
    // JSON revives Dates as strings — resurrect them.
    const at = s.at instanceof Date ? s.at : new Date(s.at);
    const keyDecisions = s.keyDecisions.map((k) => ({
        at: k.at instanceof Date ? k.at : new Date(k.at),
        summary: k.summary,
    }));
    return { ...s, at, keyDecisions };
}
function serializeWorkspaceFields(w) {
    return w;
}
function deserializeWorkspaceFields(w) {
    const createdAt = w.createdAt instanceof Date ? w.createdAt : new Date(w.createdAt);
    return { ...w, createdAt };
}
// ID generation --------------------------------------------------------------
const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
const MAX_UNIFORM_BYTE = Math.floor(256 / ALPHABET.length) * ALPHABET.length;
/**
 * Mints an {@link ArchiveBackendRef}. Local helper rather than a `utils/id`
 * export because the ref's uniqueness + prefix is an archive-backend concern
 * — other backends may mint refs very differently (e.g. S3 keys).
 */
function generateArchiveBackendRef(length = 12) {
    let suffix = '';
    let remaining = length;
    while (remaining > 0) {
        const bytes = randomBytes(remaining + 8);
        for (const byte of bytes) {
            if (remaining <= 0)
                break;
            if (byte < MAX_UNIFORM_BYTE) {
                suffix += ALPHABET[byte % ALPHABET.length];
                remaining--;
            }
        }
    }
    return `arc_${suffix}`;
}
// FS helpers -----------------------------------------------------------------
async function atomicWriteJson(filePath, value) {
    await atomicWriteText(filePath, JSON.stringify(value, null, 2));
}
async function atomicWriteText(filePath, body) {
    const tempPath = `${filePath}.tmp`;
    try {
        await writeFile(tempPath, body, 'utf-8');
        await rename(tempPath, filePath);
    }
    catch (err) {
        await unlink(tempPath).catch(() => undefined);
        throw err;
    }
}
async function readJson(path) {
    try {
        const raw = await readFile(path, 'utf-8');
        return JSON.parse(raw);
    }
    catch (err) {
        const code = err.code;
        if (code === 'ENOENT')
            return null;
        throw err;
    }
}
async function readText(path) {
    try {
        return await readFile(path, 'utf-8');
    }
    catch (err) {
        const code = err.code;
        if (code === 'ENOENT')
            return null;
        throw err;
    }
}
//# sourceMappingURL=disk-backend.js.map