import GraphPersistencePort from "../../ports/GraphPersistencePort.js";
import WarpStream from "../../domain/stream/WarpStream.js";
import PersistenceError from "../../domain/errors/PersistenceError.js";
import WarpError from "../../domain/errors/WarpError.js";
import TreeEntryFound from "../../domain/tree/TreeEntryFound.js";
import TreeEntryMissing from "../../domain/tree/TreeEntryMissing.js";
import TreeEntryPath from "../../domain/tree/TreeEntryPath.js";
import TreeEntryPrefixBatch from "../../domain/tree/TreeEntryPrefixBatch.js";
import { validateOid, validateRef, validateLimit, validateConfigKey } from "./adapterValidation.js";
import { toBytes, defaultHash, initCryptoReady, hashBlob, hashTree, hashCommit, parseMktreeEntry, } from "./inMemoryHashing.js";
const EMPTY_TREE_OID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
function treeEntryIndex(entries) {
    const index = new Map();
    for (const entry of entries) {
        index.set(entry.path, entry);
    }
    return index;
}
function compareTreeEntryFoundPath(left, right) {
    const leftPath = left.path.value;
    const rightPath = right.path.value;
    if (leftPath < rightPath) {
        return -1;
    }
    if (leftPath > rightPath) {
        return 1;
    }
    return 0;
}
export default class InMemoryGraphAdapter extends GraphPersistencePort {
    _author;
    _clock;
    _hash;
    _cryptoReady;
    _commits = new Map();
    _blobs = new Map();
    _trees = new Map();
    _treeEntryIndexes = new Map();
    _refs = new Map();
    _config = new Map();
    constructor(options) {
        super();
        const opts = options ?? {};
        const hasInjectedHash = opts.hash !== null && opts.hash !== undefined;
        this._author = (opts.author !== undefined && opts.author.length > 0) ? opts.author : 'InMemory <inmemory@test>';
        this._clock = opts.clock ?? { now: () => Date.now() };
        this._hash = hasInjectedHash ? opts.hash : defaultHash;
        this._cryptoReady = initCryptoReady(hasInjectedHash ? opts.hash : undefined);
    }
    // -- TreePort -------------------------------------------------------------
    get emptyTree() {
        return EMPTY_TREE_OID;
    }
    async writeTree(entries) {
        await this._ensureHashReady();
        const parsed = entries.map(line => parseMktreeEntry(line));
        const oid = hashTree(this._hash, parsed);
        this._trees.set(oid, parsed);
        this._treeEntryIndexes.set(oid, treeEntryIndex(parsed));
        return oid;
    }
    async readTreeOids(treeOid) {
        validateOid(treeOid);
        if (treeOid === EMPTY_TREE_OID) {
            return {};
        }
        const entries = this._trees.get(treeOid);
        if (entries === undefined) {
            throw new PersistenceError(`Tree not found: ${treeOid}`, PersistenceError.E_MISSING_OBJECT);
        }
        const result = new Map();
        for (const e of entries) {
            result.set(e.path, e.oid);
        }
        return Object.fromEntries(result);
    }
    async readTreeEntryOid(treeOid, path) {
        validateOid(treeOid);
        if (treeOid === EMPTY_TREE_OID) {
            return new TreeEntryMissing(path);
        }
        const entry = this._requireTreeEntryIndex(treeOid).get(path.value);
        if (entry !== undefined) {
            return new TreeEntryFound({ path, oid: entry.oid });
        }
        return new TreeEntryMissing(path);
    }
    async readTreeEntryPrefix(treeOid, prefix, limit) {
        validateOid(treeOid);
        if (treeOid === EMPTY_TREE_OID) {
            return new TreeEntryPrefixBatch({ prefix, limit, entries: [] });
        }
        const entries = this._requireTreeEntries(treeOid);
        const matches = [];
        const normalizedPrefix = prefix.withoutTrailingSlash();
        const childPrefix = `${normalizedPrefix.value}/`;
        for (const entry of entries) {
            if (entry.path.startsWith(childPrefix)) {
                matches.push(new TreeEntryFound({
                    path: new TreeEntryPath(entry.path),
                    oid: entry.oid,
                }));
            }
        }
        matches.sort(compareTreeEntryFoundPath);
        return new TreeEntryPrefixBatch({ prefix, limit, entries: matches.slice(0, limit.value) });
    }
    async readTree(treeOid) {
        const oids = await this.readTreeOids(treeOid);
        const files = new Map();
        for (const [path, oid] of Object.entries(oids)) {
            files.set(path, await this.readBlob(oid));
        }
        return Object.fromEntries(files);
    }
    _requireTreeEntries(treeOid) {
        const entries = this._trees.get(treeOid);
        if (entries === undefined) {
            throw new PersistenceError(`Tree not found: ${treeOid}`, PersistenceError.E_MISSING_OBJECT);
        }
        return entries;
    }
    _requireTreeEntryIndex(treeOid) {
        const entryIndex = this._treeEntryIndexes.get(treeOid);
        if (entryIndex === undefined) {
            throw new PersistenceError(`Tree not found: ${treeOid}`, PersistenceError.E_MISSING_OBJECT);
        }
        return entryIndex;
    }
    // -- BlobPort -------------------------------------------------------------
    async writeBlob(content) {
        await this._ensureHashReady();
        const bytes = toBytes(content);
        const oid = hashBlob(this._hash, bytes);
        this._blobs.set(oid, bytes);
        return oid;
    }
    async readBlob(oid) {
        validateOid(oid);
        const buf = this._blobs.get(oid);
        if (buf === undefined) {
            throw new PersistenceError(`Blob not found: ${oid}`, PersistenceError.E_MISSING_OBJECT);
        }
        return buf;
    }
    // -- CommitPort -----------------------------------------------------------
    async commitNode({ message, parents = [] }) {
        for (const p of parents) {
            validateOid(p);
        }
        return await this._createCommit(EMPTY_TREE_OID, parents, message);
    }
    async commitNodeWithTree({ treeOid, parents = [], message }) {
        validateOid(treeOid);
        for (const p of parents) {
            validateOid(p);
        }
        return await this._createCommit(treeOid, parents, message);
    }
    async showNode(sha) {
        validateOid(sha);
        const commit = this._commits.get(sha);
        if (commit === undefined) {
            throw new PersistenceError(`Commit not found: ${sha}`, PersistenceError.E_MISSING_OBJECT);
        }
        return commit.message;
    }
    async getNodeInfo(sha) {
        validateOid(sha);
        const commit = this._commits.get(sha);
        if (commit === undefined) {
            throw new PersistenceError(`Commit not found: ${sha}`, PersistenceError.E_MISSING_OBJECT);
        }
        return {
            sha,
            message: commit.message,
            author: commit.author,
            date: commit.date,
            parents: [...commit.parents],
        };
    }
    async getCommitTree(sha) {
        validateOid(sha);
        const commit = this._commits.get(sha);
        if (commit === undefined) {
            throw new PersistenceError(`Commit not found: ${sha}`, PersistenceError.E_MISSING_OBJECT);
        }
        return commit.treeOid;
    }
    async nodeExists(sha) {
        validateOid(sha);
        return this._commits.has(sha);
    }
    async countNodes(ref) {
        validateRef(ref);
        const tip = this._resolveRef(ref);
        if (tip === null) {
            throw new PersistenceError(`Ref not found: ${ref}`, PersistenceError.E_REF_NOT_FOUND);
        }
        return this._countReachable(tip);
    }
    async logNodes({ ref, limit = 50, format: _format }) {
        validateRef(ref);
        validateLimit(limit);
        const records = this._walkLog(ref, limit);
        if (typeof _format !== 'string' || _format.length === 0) {
            return records.map(c => `commit ${c.sha}\nAuthor: ${c.author}\nDate:   ${c.date}\n\n    ${c.message}\n`).join('\n');
        }
        return records.map(c => this._formatCommitRecord(c)).join('\0') + (records.length > 0 ? '\0' : '');
    }
    async logNodesStream({ ref, limit = 1000000, format: _format }) {
        validateRef(ref);
        validateLimit(limit);
        const records = this._walkLog(ref, limit);
        const formatted = records.map(c => this._formatCommitRecord(c)).join('\0') + (records.length > 0 ? '\0' : '');
        return WarpStream.of(formatted);
    }
    async ping() {
        return { ok: true, latencyMs: 0 };
    }
    // -- RefPort --------------------------------------------------------------
    async updateRef(ref, oid) {
        validateRef(ref);
        validateOid(oid);
        this._refs.set(ref, oid);
    }
    async readRef(ref) {
        validateRef(ref);
        return this._refs.get(ref) ?? null;
    }
    async deleteRef(ref) {
        validateRef(ref);
        this._refs.delete(ref);
    }
    async compareAndSwapRef(ref, newOid, expectedOid) {
        validateRef(ref);
        validateOid(newOid);
        if (typeof expectedOid === 'string' && expectedOid.length > 0) {
            validateOid(expectedOid);
        }
        const current = this._refs.get(ref) ?? null;
        if (current !== expectedOid) {
            const display = (v) => (typeof v === 'string' && v.length > 0 ? v : '(none)');
            throw new PersistenceError(`CAS mismatch on ${ref}: expected ${display(expectedOid)}, got ${display(current)}`, PersistenceError.E_REF_IO);
        }
        this._refs.set(ref, newOid);
    }
    async listRefs(prefix, options) {
        validateRef(prefix);
        const sorted = this._filterRefsByPrefix(prefix);
        const limit = options?.limit;
        if (typeof limit === 'number' && limit > 0) {
            validateLimit(limit);
            return sorted.slice(0, limit);
        }
        return sorted;
    }
    // -- ConfigPort -----------------------------------------------------------
    async configGet(key) {
        validateConfigKey(key);
        return this._config.get(key) ?? null;
    }
    async configSet(key, value) {
        validateConfigKey(key);
        if (typeof value !== 'string') {
            throw new WarpError('Config value must be a string', 'E_INVALID_INPUT');
        }
        this._config.set(key, value);
    }
    // -- Private helpers ------------------------------------------------------
    _filterRefsByPrefix(prefix) {
        const result = [];
        for (const key of this._refs.keys()) {
            if (key.startsWith(prefix)) {
                result.push(key);
            }
        }
        return result.sort();
    }
    async _createCommit(treeOid, parents, message) {
        await this._ensureHashReady();
        const date = new Date(this._clock.now()).toISOString();
        const sha = hashCommit(this._hash, { treeOid, parents, message, author: this._author, date });
        this._commits.set(sha, { treeOid, parents: [...parents], message, author: this._author, date });
        return sha;
    }
    async _ensureHashReady() {
        if (await this._cryptoReady) {
            return;
        }
        throw new WarpError('No hash function available. Pass { hash } to InMemoryGraphAdapter constructor.', 'E_NO_HASH');
    }
    _resolveRef(ref) {
        const fromRefs = this._refs.get(ref);
        if (fromRefs !== undefined) {
            return fromRefs;
        }
        if (this._commits.has(ref)) {
            return ref;
        }
        return null;
    }
    _walkLog(ref, limit) {
        const tip = this._resolveRef(ref);
        if (tip === null) {
            return [];
        }
        const all = this._collectCommits(tip);
        all.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
        return all.slice(0, limit);
    }
    _countReachable(startSha) {
        const visited = new Set();
        let count = 0;
        const stack = [startSha];
        while (stack.length > 0) {
            const sha = stack.pop();
            if (visited.has(sha)) {
                continue;
            }
            visited.add(sha);
            const commit = this._commits.get(sha);
            if (commit !== undefined) {
                count++;
                for (const p of commit.parents) {
                    stack.push(p);
                }
            }
        }
        return count;
    }
    _collectCommits(startSha) {
        const all = [];
        const visited = new Set();
        const queue = [startSha];
        let head = 0;
        while (head < queue.length) {
            const sha = queue[head++];
            if (visited.has(sha)) {
                continue;
            }
            visited.add(sha);
            const commit = this._commits.get(sha);
            if (commit !== undefined) {
                all.push({ sha, ...commit });
                for (const p of commit.parents) {
                    if (!visited.has(p)) {
                        queue.push(p);
                    }
                }
            }
        }
        return all;
    }
    _formatCommitRecord(c) {
        return `${c.sha}\n${c.author}\n${c.date}\n${c.parents.join(' ')}\n${c.message}`;
    }
}
