/**
 * StrandCoordinator — thin coordinator implementing StrandCapability.
 *
 * Composes StrandDescriptorStore, StrandMaterializer, StrandPatchService,
 * and StrandIntentService. Owns lifecycle orchestration (create, braid,
 * get, list, drop) and delegates everything else to the sub-services.
 *
 * Replaces StrandService.js (dissolved, not split).
 */
import StrandError from "../../errors/StrandError.js";
import { createImmutableTickReceiptArraySnapshot, createImmutableWarpStateSnapshot, } from "../ImmutableSnapshot.js";
import { normalizeCreateOptions, normalizeLamportCeiling, normalizeWritable, normalizeBraidedStrandIds, patchTouchesEntity, frontierToRecord, } from "./StrandDescriptorValidation.js";
import { STRAND_SCHEMA_VERSION, STRAND_COORDINATE_VERSION, STRAND_OVERLAY_KIND, normalizeOptionalString, } from "./strandShared.js";
import { buildStrandsPrefix } from "../../utils/RefLayout.js";
import { computeChecksum } from "../../utils/checksumUtils.js";
// Re-export constants that were on StrandService
export { STRAND_SCHEMA_VERSION, STRAND_COORDINATE_VERSION, STRAND_OVERLAY_KIND };
function buildStrandDescriptor({ graphName, now, frontierRecord, frontierDigest, normalized, }) {
    return {
        schemaVersion: STRAND_SCHEMA_VERSION,
        strandId: normalized.strandId,
        graphName,
        createdAt: now,
        updatedAt: now,
        owner: normalized.owner,
        scope: normalized.scope,
        lease: {
            expiresAt: normalized.leaseExpiresAt,
        },
        baseObservation: {
            coordinateVersion: STRAND_COORDINATE_VERSION,
            frontier: frontierRecord,
            frontierDigest,
            lamportCeiling: normalized.lamportCeiling,
        },
        overlay: {
            overlayId: normalized.strandId,
            kind: STRAND_OVERLAY_KIND,
            headPatchSha: null,
            patchCount: 0,
            writable: true,
        },
        braid: {
            readOverlays: [],
        },
        intentQueue: {
            nextIntentSeq: 1,
            intents: [],
        },
        evolution: {
            tickCount: 0,
            lastTick: null,
        },
        materialization: {
            cacheAuthority: 'derived',
        },
    };
}
export default class StrandCoordinator {
    _deps;
    constructor(deps) {
        this._deps = deps;
    }
    // ── Test seam accessors (private-by-convention) ─────────────────
    get _graph() { return this._deps.graph; }
    get _descriptorStore() { return this._deps.descriptors; }
    get _materializer() { return this._deps.materializer; }
    get _patchService() { return this._deps.patches; }
    get _intentService() { return this._deps.intents; }
    // ── Test seam delegation methods ─────────────────────────────────
    _buildRef(strandId) { return this._deps.descriptors.buildRef(strandId); }
    _buildOverlayRef(strandId) { return this._deps.descriptors.buildOverlayRef(strandId); }
    _buildBraidPrefix(strandId) { return this._deps.descriptors.buildBraidPrefix(strandId); }
    _writeDescriptor(descriptor) { return this._deps.descriptors.writeDescriptor(descriptor); }
    _hydrateOverlayMetadata(descriptor) {
        return this._deps.descriptors.hydrateDescriptor(descriptor);
    }
    _collectPatchEntries(descriptor, options) {
        return this._deps.materializer.collectPatchEntries(descriptor, options);
    }
    _materializeDescriptor(descriptor, options) {
        return this._deps.materializer.materializeDescriptor(descriptor, options);
    }
    _commitQueuedPatch(params) {
        return this._deps.patches.commitQueuedPatch(params);
    }
    // ── Lifecycle (owns the logic) ──────────────────────────────────
    async create(options = {}) {
        const normalized = normalizeCreateOptions(options);
        await this._assertStrandDoesNotExist(normalized.strandId);
        const frontier = await this._getFrontier();
        const frontierRecord = frontierToRecord(frontier);
        const frontierDigest = await computeChecksum(frontierRecord, this._deps.crypto);
        const now = String(this._deps.maxObservedLamport());
        const descriptor = buildStrandDescriptor({
            graphName: this._deps.graphName,
            now,
            frontierRecord,
            frontierDigest,
            normalized,
        });
        await this._deps.descriptors.writeDescriptor(descriptor);
        return descriptor;
    }
    async braid(strandId, options = {}) {
        const target = await this.getOrThrow(strandId);
        const braidedIds = normalizeBraidedStrandIds(options.braidedStrandIds, target.strandId);
        const writableOverride = normalizeWritable(options.writable);
        const readOverlays = await this._deps.descriptors.loadBraidedReadOverlays(target, braidedIds);
        await this._deps.descriptors.syncBraidRefs(target.strandId, readOverlays);
        const nextDescriptor = {
            ...target,
            updatedAt: String(this._deps.maxObservedLamport()),
            overlay: {
                ...target.overlay,
                writable: writableOverride !== null ? writableOverride : target.overlay.writable,
            },
            braid: {
                readOverlays,
            },
        };
        await this._deps.descriptors.writeDescriptor(nextDescriptor);
        return nextDescriptor;
    }
    async get(strandId) {
        const ref = this._deps.descriptors.buildRef(strandId);
        const oid = await this._deps.persistence.readRef(ref);
        if (oid === null || oid === undefined) {
            return null;
        }
        const descriptor = await this._deps.descriptors.readDescriptorByOid(oid, strandId);
        return await this._deps.descriptors.hydrateDescriptor(descriptor);
    }
    async list() {
        const prefix = buildStrandsPrefix(this._deps.graphName);
        const refs = await this._deps.persistence.listRefs(prefix);
        const ids = refs
            .map((ref) => ref.slice(prefix.length))
            .filter(Boolean)
            .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
        const descriptors = [];
        for (const id of ids) {
            const descriptor = await this.get(id);
            if (descriptor) {
                descriptors.push(descriptor);
            }
        }
        return descriptors;
    }
    async drop(strandId) {
        const ref = this._deps.descriptors.buildRef(strandId);
        const overlayRef = this._deps.descriptors.buildOverlayRef(strandId);
        const braidPrefix = this._deps.descriptors.buildBraidPrefix(strandId);
        const oid = await this._deps.persistence.readRef(ref);
        const overlayHeadSha = await this._deps.persistence.readRef(overlayRef);
        const braidRefs = await this._deps.persistence.listRefs(braidPrefix);
        const hasOid = oid !== null && oid !== undefined;
        const hasOverlaySha = overlayHeadSha !== null && overlayHeadSha !== undefined;
        if (!hasOid && !hasOverlaySha && braidRefs.length === 0) {
            return false;
        }
        for (const braidRef of braidRefs) {
            await this._deps.persistence.deleteRef(braidRef);
        }
        if (hasOverlaySha) {
            await this._deps.persistence.deleteRef(overlayRef);
        }
        if (hasOid) {
            await this._deps.persistence.deleteRef(ref);
        }
        return true;
    }
    async getOrThrow(strandId) {
        const descriptor = await this.get(strandId);
        if (!descriptor) {
            throw new StrandError(`Strand '${strandId}' not found`, {
                code: 'E_STRAND_NOT_FOUND',
                context: { graphName: this._deps.graphName, strandId },
            });
        }
        return descriptor;
    }
    // ── Materialization (delegates) ─────────────────────────────────
    async materializeLiveState(strandId, options = {}) {
        const descriptor = await this.getOrThrow(strandId);
        const ceiling = normalizeLamportCeiling(options.ceiling);
        const { state, receipts } = await this._deps.materializer.materializeDescriptor(descriptor, {
            collectReceipts: options.receipts === true,
            ceiling,
            parentBasis: 'live',
        });
        return Object.freeze({ state, receipts });
    }
    async materializeReadState(strandId, options = {}) {
        const descriptor = await this.getOrThrow(strandId);
        const ceiling = normalizeLamportCeiling(options.ceiling);
        const { state, receipts } = await this._deps.materializer.materializeDescriptor(descriptor, {
            collectReceipts: options.receipts === true,
            ceiling,
            parentBasis: this._readParentBasisFor(descriptor),
        });
        return Object.freeze({ state, receipts });
    }
    async materialize(strandId, options = {}) {
        const { state, receipts } = await this.materializeReadState(strandId, options);
        if (options.receipts === true) {
            return Object.freeze({
                state: createImmutableWarpStateSnapshot(state),
                receipts: createImmutableTickReceiptArraySnapshot(receipts),
            });
        }
        return createImmutableWarpStateSnapshot(state);
    }
    _readParentBasisFor(descriptor) {
        if (descriptor.overlay.headPatchSha !== null && descriptor.overlay.headPatchSha !== undefined) {
            return 'pinned';
        }
        if ((descriptor.braid?.readOverlays ?? []).length > 0) {
            return 'pinned';
        }
        return 'live';
    }
    // ── Patching (delegates) ────────────────────────────────────────
    async createPatchBuilder(strandId) {
        return await this._deps.patches.createPatchBuilder(strandId);
    }
    async patch(strandId, build) {
        return await this._deps.patches.patch(strandId, build);
    }
    async getPatchEntries(strandId, options = {}) {
        const descriptor = await this.getOrThrow(strandId);
        const ceiling = normalizeLamportCeiling(options.ceiling);
        return await this._deps.materializer.collectPatchEntries(descriptor, {
            ceiling,
            parentBasis: this._readParentBasisFor(descriptor),
        });
    }
    async patchesFor(strandId, entityId, options = {}) {
        const id = normalizeOptionalString(entityId, 'entityId');
        if (id === null) {
            throw new StrandError('entityId must not be empty', {
                code: 'E_STRAND_INVALID_ARGS',
                context: { field: 'entityId' },
            });
        }
        const entries = await this.getPatchEntries(strandId, options);
        const shas = new Set();
        for (const { patch, sha } of entries) {
            if (patchTouchesEntity(patch, id)) {
                shas.add(sha);
            }
        }
        return [...shas].sort();
    }
    // ── Intents (delegates) ─────────────────────────────────────────
    async queueIntent(strandId, build) {
        return await this._deps.intents.queueIntent(strandId, build);
    }
    async listIntents(strandId) {
        return await this._deps.intents.listIntents(strandId);
    }
    async tick(strandId) {
        return await this._deps.intents.tick(strandId);
    }
    // ── Private ─────────────────────────────────────────────────────
    async _assertStrandDoesNotExist(strandId) {
        const ref = this._deps.descriptors.buildRef(strandId);
        const existing = await this._deps.persistence.readRef(ref);
        if (existing !== null && existing !== undefined) {
            throw new StrandError(`Strand '${strandId}' already exists`, {
                code: 'E_STRAND_ALREADY_EXISTS',
                context: { graphName: this._deps.graphName, strandId },
            });
        }
    }
    async _getFrontier() {
        // Fetch the current frontier via persistence refs
        const prefix = buildStrandsPrefix(this._deps.graphName);
        // Use the graph's getFrontier if available (test seam)
        const { graph } = this._deps;
        if (graph !== undefined && typeof graph.getFrontier === 'function') {
            return await graph.getFrontier();
        }
        // Fallback: return empty frontier
        void prefix;
        return new Map();
    }
}
