/**
 * QueryController — read-only query surface for materialized graph state.
 *
 * Thin facade composing QueryReads (graph reads), QueryContent
 * (blob access), and query/observer/worldline factory methods.
 */
import { cloneState } from "../JoinReducer.js";
import LiveQueryReadModelProvider from "../query/LiveQueryReadModelProvider.js";
import Observer from "../query/Observer.js";
import ProjectionHandle from "../ProjectionHandle.js";
import WorldlineOptic from "../optic/WorldlineOptic.js";
import { computeTranslationCost } from "../TranslationCost.js";
import { toInternalStrandShape } from "../../utils/strandPublicShape.js";
import WorldlineSelector from "../../types/WorldlineSelector.js";
import LiveSelector from "../../types/LiveSelector.js";
import CoordinateSelector from "../../types/CoordinateSelector.js";
import StrandSelector from "../../types/StrandSelector.js";
import QueryError from "../../errors/QueryError.js";
import CheckpointTailExactIdQueryReadModel, { exactIdOnlyQueryNodeId, } from "../query/CheckpointTailExactIdQueryReadModel.js";
import { E_NO_STATE_MSG } from "./QueryStateMessages.js";
import { hasNodeImpl, getNodePropsImpl, getEdgePropsImpl, neighborsImpl, getStateSnapshotImpl, getNodesImpl, getEdgesImpl, getPropertyCountImpl, } from "./QueryReads.js";
import { getContentOidImpl, getContentMetaImpl, getContentImpl, getContentStreamImpl, getEdgeContentOidImpl, getEdgeContentMetaImpl, getEdgeContentImpl, getEdgeContentStreamImpl, } from "./QueryContent.js";
function toSelector(source) {
    if (!source) {
        return undefined;
    }
    return WorldlineSelector.from(source).clone();
}
async function snapshotCurrent(deps) {
    const graph = deps.hostGraph;
    await graph._ensureFreshState();
    const state = graph._cachedState;
    if (state === null) {
        throw new QueryError(E_NO_STATE_MSG, { code: 'E_NO_STATE' });
    }
    return { state: cloneState(state), stateHash: await deps.hashState(state) };
}
function hasLiveFrontierHost(graph) {
    return graph._lastFrontier instanceof Map
        && typeof graph._stateDirty === 'boolean'
        && typeof graph.getFrontier === 'function';
}
function frontiersEqual(left, right) {
    if (left.size !== right.size) {
        return false;
    }
    for (const [writerId, tipSha] of left.entries()) {
        if (right.get(writerId) !== tipSha) {
            return false;
        }
    }
    return true;
}
async function hasFreshCachedLiveSnapshot(deps) {
    const graph = deps.hostGraph;
    if (graph._cachedState === null || !hasLiveFrontierHost(graph) || graph._stateDirty) {
        return false;
    }
    const lastFrontier = graph._lastFrontier;
    if (lastFrontier === null) {
        return false;
    }
    return frontiersEqual(await graph.getFrontier(), lastFrontier);
}
async function resolveSnapshot(deps, options) {
    const source = toSelector(options?.source);
    if (!source) {
        return await snapshotCurrent(deps);
    }
    return await resolveSourceSnapshot(deps, source);
}
async function resolveSourceSnapshot(deps, source) {
    if (source instanceof LiveSelector) {
        return await resolveLiveSnapshot(deps, source);
    }
    if (source instanceof CoordinateSelector) {
        return await resolveCoordinateSnapshot(deps, source);
    }
    if (source instanceof StrandSelector) {
        return await resolveStrandSnapshot(deps, source);
    }
    throw new QueryError(`unrecognized observer source kind: ${source.constructor.name}`, {
        code: 'E_OBSERVER_SOURCE_UNKNOWN',
        context: { sourceKind: source.constructor.name },
    });
}
async function openDetachedObserverGraph(deps) {
    return await deps.graphCloner.openReadOnly();
}
async function resolveLiveSnapshot(deps, source) {
    if (source.ceiling === null && await hasFreshCachedLiveSnapshot(deps)) {
        return await snapshotCurrent(deps);
    }
    const detached = await openDetachedObserverGraph(deps);
    const materialized = await detached._materializeGraph({ ceiling: source.ceiling ?? null });
    return {
        state: cloneState(materialized.state),
        stateHash: materialized.stateHash,
    };
}
async function resolveCoordinateSnapshot(deps, source) {
    const detached = await openDetachedObserverGraph(deps);
    const materialized = await detached._materializeCoordinateGraph({ frontier: source.frontier, ceiling: source.ceiling ?? null });
    return {
        state: cloneState(materialized.state),
        stateHash: materialized.stateHash,
    };
}
async function resolveStrandSnapshot(deps, source) {
    const detached = await openDetachedObserverGraph(deps);
    const internal = toInternalStrandShape(source.toDTO());
    const materialized = await detached._materializeStrandGraph(internal.strandId, {
        ceiling: internal.ceiling ?? null,
    });
    return {
        state: cloneState(materialized.state),
        stateHash: materialized.stateHash,
    };
}
function isObserverConfig(value) {
    return value !== undefined && 'match' in value;
}
function observerOptions(value) {
    return isObserverConfig(value) ? undefined : value;
}
function normalizeObserverArgs(nameOrConfig, configOrOptions, maybeOptions) {
    if (typeof nameOrConfig === 'string') {
        return {
            name: nameOrConfig,
            config: isObserverConfig(configOrOptions) ? configOrOptions : undefined,
            options: maybeOptions,
        };
    }
    return {
        name: 'observer',
        config: nameOrConfig,
        options: observerOptions(configOrOptions),
    };
}
function isValidMatch(m) {
    if (typeof m === 'string') {
        return true;
    }
    return Array.isArray(m) && m.length > 0 && m.every((i) => typeof i === 'string');
}
// ── Controller class ────────────────────────────────────────────────
export default class QueryController {
    _host;
    _graphCloner;
    _hashState;
    constructor(deps) {
        this._host = deps.hostGraph;
        this._graphCloner = deps.graphCloner;
        this._hashState = deps.hashState;
    }
}
// ── Wire methods via defineProperty ──────────────────────────────────
function host(ctrl) { return ctrl._host; }
function snapshotDeps(ctrl) {
    return {
        hostGraph: ctrl._host,
        graphCloner: ctrl._graphCloner,
        hashState: ctrl._hashState,
    };
}
function defaultQueryReadModelProvider(ctrl) {
    const h = host(ctrl);
    return new LiveQueryReadModelProvider({
        ensureFreshState: async () => { await h._ensureFreshState(); },
        currentState: () => h._cachedState,
        stateHash: ctrl._hashState,
        neighborProvider: () => h._materializedGraph?.provider ?? null,
        boundedReadModelProvider: async (request) => await boundedExactReadModel(h, request),
    });
}
async function boundedExactReadModel(graph, request) {
    const nodeId = exactIdOnlyQueryNodeId(request);
    if (nodeId === null || !hasWorldlineOpticSource(graph)) {
        return null;
    }
    if (await graph._readCheckpointSha() === null) {
        return null;
    }
    return new CheckpointTailExactIdQueryReadModel({
        nodeId,
        optic: new WorldlineOptic({ source: graph }),
    });
}
function hasWorldlineOpticSource(graph) {
    return typeof graph.graphName === 'string'
        && graph._persistence !== undefined
        && graph._codec !== undefined
        && graph._blobStorage !== undefined
        && graph._commitMessageCodec !== undefined
        && typeof graph.discoverWriters === 'function'
        && typeof graph._readCheckpointSha === 'function'
        && typeof graph._loadPatchChainFromSha === 'function'
        && typeof graph._loadWriterPatches === 'function'
        && typeof graph._validatePatchAgainstCheckpoint === 'function';
}
function worldlineOpticSource(graph) {
    return hasWorldlineOpticSource(graph) ? graph : null;
}
/**
 * CallableFunction is the least-information return shape for a
 * dispatch wrapper — the actual per-method types live on the
 * declaration-file surface exposed to callers.
 */
function wire(name, fn) {
    Object.defineProperty(QueryController.prototype, name, {
        value: fn, writable: true, configurable: true, enumerable: false,
    });
}
function wireEdge(name, impl) {
    wire(name, function (from, to, label) { return impl(host(this), { from, to, label }); });
}
// QueryReads delegates
wire('hasNode', function (nodeId) { return hasNodeImpl(host(this), nodeId); });
wire('getNodeProps', function (nodeId) { return getNodePropsImpl(host(this), nodeId); });
wire('getEdgeProps', function (from, to, label) { return getEdgePropsImpl(host(this), { from, to, label }); });
wire('neighbors', function (nodeId, direction = 'both', edgeLabel) {
    return neighborsImpl(host(this), { nodeId, direction, ...(edgeLabel !== undefined ? { edgeLabel } : {}) });
});
wire('getStateSnapshot', function () { return getStateSnapshotImpl(host(this)); });
wire('getNodes', function () { return getNodesImpl(host(this)); });
wire('getEdges', function () { return getEdgesImpl(host(this)); });
wire('getPropertyCount', function () { return getPropertyCountImpl(host(this)); });
// QueryContent delegates
wire('getContentOid', function (nodeId) { return getContentOidImpl(host(this), nodeId); });
wire('getContentMeta', function (nodeId) { return getContentMetaImpl(host(this), nodeId); });
wire('getContent', function (nodeId) { return getContentImpl(host(this), nodeId); });
wire('getContentStream', function (nodeId) { return getContentStreamImpl(host(this), nodeId); });
wireEdge('getEdgeContentOid', getEdgeContentOidImpl);
wireEdge('getEdgeContentMeta', getEdgeContentMetaImpl);
wireEdge('getEdgeContent', getEdgeContentImpl);
wireEdge('getEdgeContentStream', getEdgeContentStreamImpl);
// Factory methods
wire('query', function () {
    return new Observer({
        name: 'observer',
        config: { match: '*' },
        readModelProvider: defaultQueryReadModelProvider(this),
    }).query();
});
wire('worldline', function (options) {
    return new ProjectionHandle({
        graph: host(this),
        source: toSelector(options?.source) ?? new LiveSelector(),
        opticSource: worldlineOpticSource(host(this)),
    });
});
wire('observer', async function (nameOrConfig, configOrOptions, maybeOptions) {
    const { name, config, options } = normalizeObserverArgs(nameOrConfig, configOrOptions, maybeOptions);
    if (!config || !isValidMatch(config.match)) {
        throw new QueryError('observer config.match must be a non-empty string or non-empty array of strings', {
            code: 'E_OBSERVER_MATCH_TYPE',
        });
    }
    const h = host(this);
    const sourceSelector = options?.source !== undefined ? toSelector(options.source) : undefined;
    const snapshot = await resolveSnapshot(snapshotDeps(this), options);
    return new Observer({
        name, config, graph: h, snapshot,
        source: sourceSelector ?? new LiveSelector(),
    });
});
wire('translationCost', async function (configA, configB) {
    const h = host(this);
    await h._ensureFreshState();
    const state = h._cachedState;
    if (state === null) {
        throw new QueryError('translation cost requires current graph state', {
            code: 'E_QUERY_TRANSLATION_COST_STATE',
        });
    }
    return computeTranslationCost(configA, configB, state);
});
