import * as crypto from "crypto";
import { canonicalize } from "./canonicalize.js";
export class ContentAddressableStore {
    seenHashes = new Set();
    chunkData = {};
    process(record, schema) {
        const chunks = [];
        const result = this.walk(record, schema, chunks);
        return { record: result, chunks };
    }
    reconstruct(record, schema) {
        return this.walkReverse(record, schema);
    }
    loadChunks(chunks) {
        for (const [hash, data] of Object.entries(chunks)) {
            this.seenHashes.add(hash);
            this.chunkData[hash] = data;
        }
    }
    /**
     * Seed the set of hashes that should be treated as already-emitted, without
     * loading their chunk data. Used by `TraceWriter` when it scans an existing
     * trace file at construction time so that subsequent `process()` calls don't
     * re-emit chunks that prior writers in the same run already wrote. Unlike
     * `loadChunks`, this does NOT populate `chunkData` (the writer never needs to
     * reconstruct values, only check whether to emit them), keeping memory low
     * for long traces.
     */
    seedSeenHashes(hashes) {
        for (const h of hashes)
            this.seenHashes.add(h);
    }
    walk(data, schema, chunks) {
        if (data === null || data === undefined)
            return data;
        const result = Array.isArray(data) ? [...data] : { ...data };
        for (const key of Object.keys(schema)) {
            if (!(key in data))
                continue;
            const schemaValue = schema[key];
            if (schemaValue === true) {
                const val = data[key];
                if (Array.isArray(val)) {
                    result[key] = val.map((item) => this.hashAndStore(item, chunks));
                }
                else if (typeof val === "object" && val !== null) {
                    const hashed = {};
                    for (const k of Object.keys(val)) {
                        hashed[k] = this.hashAndStore(val[k], chunks);
                    }
                    result[key] = hashed;
                }
                else {
                    result[key] = this.hashAndStore(val, chunks);
                }
            }
            else {
                result[key] = this.walk(data[key], schemaValue, chunks);
            }
        }
        return result;
    }
    walkReverse(data, schema) {
        if (data === null || data === undefined)
            return data;
        const result = Array.isArray(data) ? [...data] : { ...data };
        for (const key of Object.keys(schema)) {
            if (!(key in data))
                continue;
            const schemaValue = schema[key];
            if (schemaValue === true) {
                const val = data[key];
                if (Array.isArray(val)) {
                    result[key] = val.map((hash) => this.chunkData[hash]);
                }
                else if (typeof val === "object" && val !== null) {
                    const resolved = {};
                    for (const k of Object.keys(val)) {
                        resolved[k] = this.chunkData[val[k]];
                    }
                    result[key] = resolved;
                }
                else {
                    result[key] = this.chunkData[val];
                }
            }
            else {
                result[key] = this.walkReverse(data[key], schemaValue);
            }
        }
        return result;
    }
    hashAndStore(value, chunks) {
        const canonical = canonicalize(value);
        const hash = crypto
            .createHash("sha256")
            .update(canonical)
            .digest("hex")
            .slice(0, 16);
        if (!this.seenHashes.has(hash)) {
            this.seenHashes.add(hash);
            this.chunkData[hash] = value;
            chunks.push({ hash, data: value });
        }
        return hash;
    }
}
