/**
 * SubscriptionController — graph change subscription and watch logic.
 *
 * Extracted from subscribe.methods.js. Manages subscriber registration,
 * glob-filtered watches with optional polling, and deferred replay.
 *
 * @module domain/services/controllers/SubscriptionController
 */
import { diffStates, isEmptyDiff } from "../state/StateDiff.js";
import { matchGlob } from "../../utils/matchGlob.js";
import WarpError from "../../errors/WarpError.js";
import QueryError from "../../errors/QueryError.js";
import { E_STALE_STATE_MSG } from "./QueryStateMessages.js";
export default class SubscriptionController {
    _host;
    _scheduler;
    constructor(host, options) {
        this._host = host;
        this._scheduler = options?.scheduler ?? null;
    }
    /** Returns the scheduler, falling back to globalThis.setInterval at call time. */
    _resolveScheduler() {
        return this._scheduler ?? globalThis.setInterval.bind(globalThis);
    }
    subscribe({ onChange, onError, replay = false }) {
        if (typeof onChange !== 'function') {
            throw new WarpError('onChange must be a function', 'E_SUBSCRIBE_INVALID_CALLBACK');
        }
        const host = this._host;
        const subscriber = {
            onChange,
            ...(onError !== undefined ? { onError } : {}),
            pendingReplay: replay && !host._cachedState,
        };
        host._subscribers.push(subscriber);
        if (replay && host._cachedState) {
            const diff = diffStates(null, host._cachedState);
            if (!isEmptyDiff(diff)) {
                try {
                    onChange(diff);
                }
                catch (err) {
                    if (onError) {
                        try {
                            onError(err instanceof Error ? err : new WarpError(String(err), 'E_SUBSCRIBE_NON_ERROR'));
                        }
                        catch {
                            // onError itself threw — swallow to prevent cascade
                        }
                    }
                }
            }
        }
        return {
            unsubscribe: () => {
                const index = host._subscribers.indexOf(subscriber);
                if (index !== -1) {
                    host._subscribers.splice(index, 1);
                }
            },
        };
    }
    watch(pattern, { onChange, onError, poll }) {
        const isValidPattern = (p) => typeof p === 'string' || (Array.isArray(p) && p.length > 0 && p.every(i => typeof i === 'string'));
        if (!isValidPattern(pattern)) {
            throw new WarpError('pattern must be a non-empty string or non-empty array of strings', 'E_WATCH_INVALID_PATTERN');
        }
        if (typeof onChange !== 'function') {
            throw new WarpError('onChange must be a function', 'E_WATCH_INVALID_CALLBACK');
        }
        if (poll !== undefined) {
            if (typeof poll !== 'number' || !Number.isFinite(poll) || poll < 1000) {
                throw new WarpError('poll must be a finite number >= 1000', 'E_WATCH_INVALID_POLL');
            }
        }
        const matchesPattern = (nodeId) => matchGlob(pattern, nodeId);
        const filteredOnChange = (diff) => {
            const filteredDiff = {
                nodes: {
                    added: diff.nodes.added.filter(matchesPattern),
                    removed: diff.nodes.removed.filter(matchesPattern),
                },
                edges: {
                    added: diff.edges.added.filter((e) => matchesPattern(e.from) || matchesPattern(e.to)),
                    removed: diff.edges.removed.filter((e) => matchesPattern(e.from) || matchesPattern(e.to)),
                },
                props: {
                    set: diff.props.set.filter((p) => matchesPattern(p.nodeId)),
                    removed: diff.props.removed.filter((p) => matchesPattern(p.nodeId)),
                },
            };
            const hasChanges = filteredDiff.nodes.added.length > 0 ||
                filteredDiff.nodes.removed.length > 0 ||
                filteredDiff.edges.added.length > 0 ||
                filteredDiff.edges.removed.length > 0 ||
                filteredDiff.props.set.length > 0 ||
                filteredDiff.props.removed.length > 0;
            if (hasChanges) {
                onChange(filteredDiff);
            }
        };
        const subscription = this.subscribe({
            onChange: filteredOnChange,
            ...(onError !== undefined ? { onError } : {}),
        });
        const host = this._host;
        let pollIntervalId = null;
        let pollInFlight = false;
        if (poll !== undefined) {
            pollIntervalId = this._resolveScheduler()(() => {
                if (pollInFlight) {
                    return;
                }
                pollInFlight = true;
                host.hasFrontierChanged()
                    .then((changed) => {
                    if (changed) {
                        throw new QueryError(E_STALE_STATE_MSG, { code: 'E_STALE_STATE' });
                    }
                })
                    .catch((err) => {
                    if (onError) {
                        try {
                            onError(err instanceof Error ? err : new WarpError(String(err), 'E_SUBSCRIBE_NON_ERROR'));
                        }
                        catch {
                            // onError itself threw — swallow
                        }
                    }
                })
                    .finally(() => {
                    pollInFlight = false;
                });
            }, poll);
        }
        return {
            unsubscribe: () => {
                if (pollIntervalId !== null) {
                    clearInterval(pollIntervalId);
                    pollIntervalId = null;
                }
                subscription.unsubscribe();
            },
        };
    }
    _notifySubscribers(diff, currentState) {
        for (const subscriber of [...this._host._subscribers]) {
            try {
                if (subscriber.pendingReplay) {
                    subscriber.pendingReplay = false;
                    const replayDiff = diffStates(null, currentState);
                    if (!isEmptyDiff(replayDiff)) {
                        subscriber.onChange(replayDiff);
                    }
                }
                else {
                    if (isEmptyDiff(diff)) {
                        continue;
                    }
                    subscriber.onChange(diff);
                }
            }
            catch (err) {
                if (typeof subscriber.onError === 'function') {
                    try {
                        subscriber.onError(err instanceof Error ? err : new WarpError(String(err), 'E_SUBSCRIBE_NON_ERROR'));
                    }
                    catch {
                        // onError itself threw — swallow
                    }
                }
            }
        }
    }
}
