// capsule-set-lifecycle.ts — CapsuleSets are LOGICAL compositions: they hold capsule IDs
// only, never vectors/chunks/documents (Foundry-IQ "no global pool" invariant).
//
// The #265 schema declares `capsule_set_members(set_id, capsule_id, ordinal, composed_at)`
// but does NOT declare a top-level `capsule_sets` table; #265 explicitly defers the set's
// metadata storage to consumers because the contract layer only knows the membership
// relation. We persist the metadata (display name, description, tags) as a single row in
// the existing `schema_meta` key-value table under key `capsule_set:<id>`, with the JSON
// payload `{displayName, description?, tags, composedAt}`. Members live in their declared
// table. Deleting a capsule cascades to the membership row via the FK from #265; this
// module does not need to chase it.
import { KnowledgeNotFoundError, KnowledgeStoreError } from "./errors.js";
const META_PREFIX = "capsule_set:";
function metaKey(id) {
    return `${META_PREFIX}${String(id)}`;
}
function parseMeta(value, id) {
    const parsed = JSON.parse(value);
    if (typeof parsed !== "object" || parsed === null) {
        throw new KnowledgeStoreError(`Corrupt schema_meta payload for ${String(id)}.`);
    }
    const obj = parsed;
    const displayName = obj.displayName;
    const description = obj.description;
    const tags = obj.tags;
    const composedAt = obj.composedAt;
    if (typeof displayName !== "string") {
        throw new KnowledgeStoreError(`schema_meta payload missing displayName for ${String(id)}.`);
    }
    if (typeof composedAt !== "number") {
        throw new KnowledgeStoreError(`schema_meta payload missing composedAt for ${String(id)}.`);
    }
    return {
        displayName,
        description: typeof description === "string" ? description : null,
        tags: Array.isArray(tags) ? tags.filter((t) => typeof t === "string") : [],
        composedAt,
    };
}
function readMembers(store, setId) {
    const rows = store._internal.db
        .prepare("SELECT capsule_id, ordinal FROM capsule_set_members WHERE set_id = :s ORDER BY ordinal ASC")
        .all({ s: setId });
    return rows.map((row) => row.capsule_id);
}
function metaToCapsuleSet(id, payload, capsuleIds) {
    const base = {
        id,
        displayName: payload.displayName,
        tags: payload.tags,
        capsuleIds,
        composedAt: payload.composedAt,
    };
    return payload.description === null ? base : { ...base, description: payload.description };
}
// Inserts the schema_meta row and all member rows WITHOUT bracketing a BEGIN/COMMIT.
// Must be called from within an already-open transaction. Used by createCapsuleSet (which
// wraps it in its own transaction) and by runComposeTransaction (which needs to combine the
// set creation and audit writes in one atomic outer transaction).
export function createCapsuleSetWithinTxn(store, input, now) {
    const db = store._internal.db;
    const payload = {
        displayName: input.displayName,
        description: input.description ?? null,
        tags: input.tags,
        composedAt: now,
    };
    db.prepare("INSERT INTO schema_meta (key, value) VALUES (:k, :v)").run({
        k: metaKey(input.id),
        v: JSON.stringify(payload),
    });
    const insertMember = db.prepare("INSERT INTO capsule_set_members (set_id, capsule_id, ordinal, composed_at) VALUES (:s, :c, :o, :now)");
    input.capsuleIds.forEach((capsuleId, index) => {
        insertMember.run({ s: input.id, c: capsuleId, o: index, now });
    });
}
export function createCapsuleSet(store, input) {
    const db = store._internal.db;
    const now = store._internal.now();
    db.exec("BEGIN");
    try {
        createCapsuleSetWithinTxn(store, input, now);
        db.exec("COMMIT");
    }
    catch (error) {
        db.exec("ROLLBACK");
        const msg = error instanceof Error ? error.message : String(error);
        if (/UNIQUE|PRIMARY KEY/i.test(msg)) {
            throw new KnowledgeStoreError("capsule set already exists", { cause: error });
        }
        throw new KnowledgeStoreError("failed to create capsule set", { cause: error });
    }
    const fetched = getCapsuleSet(store, input.id);
    if (fetched === undefined) {
        throw new KnowledgeStoreError(`createCapsuleSet: insert succeeded but row not found for ${String(input.id)}`);
    }
    return fetched;
}
export function getCapsuleSet(store, id) {
    const row = store._internal.db
        .prepare("SELECT value FROM schema_meta WHERE key = :k")
        .get({ k: metaKey(id) });
    if (row === undefined)
        return undefined;
    const payload = parseMeta(row.value, id);
    const members = readMembers(store, id);
    return metaToCapsuleSet(id, payload, members);
}
export function listCapsuleSets(store) {
    // Fetch (key, value) in one query so we avoid a getCapsuleSet re-query per row (N+1).
    // Members still need a per-set query because capsule_set_members is a separate table.
    const rows = store._internal.db
        .prepare("SELECT key, value FROM schema_meta WHERE key LIKE :prefix")
        .all({ prefix: `${META_PREFIX}%` });
    const sets = rows
        .map((row) => {
        const { key, value } = row;
        const id = key.slice(META_PREFIX.length);
        const payload = parseMeta(value, id);
        const capsuleIds = readMembers(store, id);
        return metaToCapsuleSet(id, payload, capsuleIds);
    });
    return sets.slice().sort((a, b) => a.composedAt - b.composedAt);
}
export function deleteCapsuleSet(store, id) {
    const db = store._internal.db;
    db.exec("BEGIN");
    try {
        const result = db.prepare("DELETE FROM schema_meta WHERE key = :k").run({ k: metaKey(id) });
        if (Number(result.changes) === 0) {
            db.exec("ROLLBACK");
            throw new KnowledgeNotFoundError(`Capsule set not found: ${String(id)}`);
        }
        db.prepare("DELETE FROM capsule_set_members WHERE set_id = :s").run({ s: id });
        db.exec("COMMIT");
    }
    catch (error) {
        if (!(error instanceof KnowledgeNotFoundError)) {
            db.exec("ROLLBACK");
        }
        throw error;
    }
}
