/**
 * Observer - Read-only filtered view of a causal read model.
 *
 * Provides an observer that sees only nodes matching a glob pattern,
 * with property visibility controlled by expose/redact lists.
 * Edges are only visible when both endpoints pass the match filter.
 *
 * @module domain/services/query/Observer
 * @see Paper IV, Section 3 -- Observers as resource-bounded functors
 */
import QueryBuilder from "./QueryBuilder.js";
import StateQueryReadModel from "./StateQueryReadModel.js";
import VisibleQueryReadModel from "./VisibleQueryReadModel.js";
import LogicalTraversal from "./LogicalTraversal.js";
import ObserverAccumulation from "./ObserverAccumulation.js";
import ObserverBasis from "./ObserverBasis.js";
import ObserverPlan from "./ObserverPlan.js";
import ObserverReadingEnvelope from "./ObserverReadingEnvelope.js";
import { createStateReader } from "../state/StateReader.js";
import { matchGlob } from "../../utils/matchGlob.js";
import QueryError from "../../errors/QueryError.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";
function toSelector(source) {
    if (source === null || source === undefined) {
        return null;
    }
    return WorldlineSelector.from(source).clone();
}
function selectorToSource(source) {
    if (source instanceof LiveSelector) {
        return source.toDTO();
    }
    if (source instanceof CoordinateSelector) {
        return source.toDTO();
    }
    if (source instanceof StrandSelector) {
        return source.toDTO();
    }
    throw new QueryError(`unrecognized observer source kind: ${source.constructor.name}`, {
        code: 'E_OBSERVER_SOURCE_UNKNOWN',
        context: { sourceKind: source.constructor.name },
    });
}
function toFilterSet(list) {
    return Array.isArray(list) && list.length > 0 ? new Set(list) : null;
}
function isKeyVisible(key, redactSet, exposeSet) {
    if (redactSet !== null && redactSet.has(key)) {
        return false;
    }
    if (exposeSet !== null && !exposeSet.has(key)) {
        return false;
    }
    return true;
}
function filterProps(propsRecord, expose, redact) {
    const redactSet = toFilterSet(redact);
    const exposeSet = toFilterSet(expose);
    const filtered = {};
    for (const [key, value] of Object.entries(propsRecord)) {
        if (isKeyVisible(key, redactSet, exposeSet)) {
            filtered[key] = value;
        }
    }
    return Object.freeze(filtered);
}
/**
 * Read-only observer view over a live read model or pinned state snapshot.
 */
export default class Observer {
    _name;
    _matchPattern;
    _expose;
    _redact;
    _basis;
    _graph;
    _snapshot;
    _source;
    _stateReader;
    _readModelProvider;
    traverse;
    constructor({ name, config, graph, snapshot, source, readModelProvider }) {
        this._initIdentity(name, config);
        this._initBacking(graph, snapshot, source, readModelProvider);
        this.traverse = new LogicalTraversal(this.queryReadModelProvider());
    }
    _initIdentity(name, config) {
        this._name = name ?? 'observer';
        this._matchPattern = Array.isArray(config.match) ? [...config.match] : config.match;
        this._expose = config.expose ? [...config.expose] : undefined;
        this._redact = config.redact ? [...config.redact] : undefined;
        this._basis = ObserverBasis.from(config.basis);
    }
    _initBacking(graph, snapshot, source, readModelProvider) {
        this._graph = graph ?? null;
        this._snapshot = snapshot ?? null;
        this._source = toSelector(source ?? new LiveSelector());
        this._stateReader = snapshot ? createStateReader(snapshot.state) : null;
        this._readModelProvider = readModelProvider ?? null;
    }
    get name() {
        return this._name;
    }
    get source() {
        return this._source ? selectorToSource(this._source) : null;
    }
    get stateHash() {
        return this._snapshot ? this._snapshot.stateHash : null;
    }
    get basis() {
        return this._basis;
    }
    _requireGraph() {
        if (!this._graph) {
            throw new QueryError('Observer has no live backing graph', { code: 'E_OBSERVER_NO_GRAPH' });
        }
        return this._graph;
    }
    _buildConfigSnapshot() {
        const config = {
            match: Array.isArray(this._matchPattern) ? [...this._matchPattern] : this._matchPattern,
        };
        if (this._expose) {
            config.expose = [...this._expose];
        }
        if (this._redact) {
            config.redact = [...this._redact];
        }
        if (!this._basis.isEmpty()) {
            config.basis = this._basis.toConfigValue();
        }
        return config;
    }
    async seek(options) {
        const graph = this._requireGraph();
        const config = this._buildConfigSnapshot();
        const nextSource = options?.source
            ? WorldlineSelector.from(options.source).clone()
            : new LiveSelector();
        return await graph.observer(this._name, config, { source: selectorToSource(nextSource) });
    }
    // ===========================================================================
    // Query read model provider
    // ===========================================================================
    queryReadModelProvider() {
        return this;
    }
    async openQueryReadModel(request) {
        if (this._readModelProvider !== null) {
            return new VisibleQueryReadModel({
                source: await this._readModelProvider.openQueryReadModel(request),
                visibility: this._queryVisibility(),
            });
        }
        if (this._snapshot !== null) {
            return new StateQueryReadModel({
                state: this._snapshot.state,
                stateHash: this._snapshot.stateHash,
                visibility: this._queryVisibility(),
            });
        }
        throw new QueryError('Observer query requires a snapshot or query read-model provider', {
            code: 'E_OBSERVER_QUERY_READ_MODEL',
        });
    }
    _queryVisibility() {
        return {
            match: Array.isArray(this._matchPattern) ? [...this._matchPattern] : this._matchPattern,
            ...(this._expose !== undefined ? { expose: [...this._expose] } : {}),
            ...(this._redact !== undefined ? { redact: [...this._redact] } : {}),
        };
    }
    // ===========================================================================
    // Structural observer API
    // ===========================================================================
    async accumulate() {
        let accumulation = ObserverAccumulation.empty(this._basis);
        const nodeIds = await this.getNodes();
        for (const nodeId of nodeIds) {
            const props = await this.getNodeProps(nodeId);
            if (props !== null) {
                accumulation = accumulation.includeNode(props);
            }
        }
        return accumulation.includeEdges((await this.getEdges()).length);
    }
    async emit() {
        return (await this.accumulate()).emit();
    }
    plan() {
        return new ObserverPlan({
            name: this._name,
            match: Array.isArray(this._matchPattern) ? [...this._matchPattern] : this._matchPattern,
            ...(this._expose !== undefined ? { expose: [...this._expose] } : {}),
            ...(this._redact !== undefined ? { redact: [...this._redact] } : {}),
            basis: this._basis,
            source: this._source ?? new LiveSelector(),
        });
    }
    async readingEnvelope(options = {}) {
        return new ObserverReadingEnvelope({
            plan: this.plan(),
            payload: await this.emit(),
            stateHash: this.stateHash,
            ...options,
        });
    }
    // ===========================================================================
    // Node API
    // ===========================================================================
    async hasNode(nodeId) {
        if (!matchGlob(this._matchPattern, nodeId)) {
            return false;
        }
        if (this._stateReader) {
            return this._stateReader.hasNode(nodeId);
        }
        return await this._requireGraph().hasNode(nodeId);
    }
    async getNodes() {
        const allNodes = this._stateReader
            ? this._stateReader.getNodes()
            : await this._requireGraph().getNodes();
        return allNodes.filter((id) => matchGlob(this._matchPattern, id));
    }
    async getNodeProps(nodeId) {
        if (!matchGlob(this._matchPattern, nodeId)) {
            return null;
        }
        const propsRecord = this._stateReader
            ? this._stateReader.getNodeProps(nodeId)
            : await this._requireGraph().getNodeProps(nodeId);
        if (!propsRecord) {
            return null;
        }
        return filterProps(propsRecord, this._expose, this._redact);
    }
    // ===========================================================================
    // Edge API
    // ===========================================================================
    async getEdges() {
        const allEdges = this._stateReader
            ? this._stateReader.getEdges()
            : await this._requireGraph().getEdges();
        return allEdges
            .filter((e) => matchGlob(this._matchPattern, e.from) && matchGlob(this._matchPattern, e.to))
            .map((e) => {
            const filtered = filterProps(e.props, this._expose, this._redact);
            return { ...e, props: filtered };
        });
    }
    // ===========================================================================
    // Query API
    // ===========================================================================
    query() {
        return new QueryBuilder(this.queryReadModelProvider());
    }
}
