import { DEFAULT_MAX_TRANSITIONS, DEFAULT_TIMELINE_BUFFER_SIZE } from './types.js';
import { BoundedTransitionLog } from './transition-log.js';
import { TimelineBuffer } from './timeline.js';
import { serializeSafe } from './serialize.js';
import { summarizeError } from '../../../utils/error-display.js';
// ── StateInspectorProvider ────────────────────────────────────────────────────
/**
 * StateInspectorProvider — full-featured state inspector data provider.
 *
 * ### Usage
 * ```ts
 * const inspector = createStateInspector({
 *   domains: [sessionDomainAdapter, conversationDomainAdapter],
 *   maxTransitions: 500,
 * });
 *
 * const snapshot = inspector.getSnapshot();
 * const history = inspector.getTransitionHistory();
 * const subs = inspector.getSubscriptions();
 * ```
 */
export class StateInspectorProvider {
    _domains;
    _transitionLog;
    _timeline;
    _observedDomains;
    _subscriptions = new Map();
    _domainCache = new Map();
    _subIdCounter = 0;
    /**
     * @param domains - Domain adapters to inspect.
     * @param config - Optional configuration.
     */
    constructor(domains = [], config = {}) {
        this._domains = [...domains];
        this._transitionLog = new BoundedTransitionLog(config.maxTransitions ?? DEFAULT_MAX_TRANSITIONS);
        this._timeline = new TimelineBuffer(config.timelineBufferSize ?? DEFAULT_TIMELINE_BUFFER_SIZE);
        this._observedDomains = config.observedDomains
            ? new Set(config.observedDomains)
            : undefined;
    }
    // ── Domain management ───────────────────────────────────────────────────────
    /**
     * Register an additional domain for inspection.
     * Triggers a notification to all subscribers.
     *
     * @param domain - Domain adapter to register.
     */
    registerDomain(domain) {
        this._domains.push(domain);
        this._notifyAll();
    }
    /** Returns the names of all currently registered domains. */
    registeredDomainNames() {
        return this._domains.map((d) => d.name);
    }
    // ── Snapshot API ────────────────────────────────────────────────────────────
    /**
     * Capture a point-in-time snapshot of all (or filtered) domains.
     *
     * @param domainFilter - Optional list of domain names to include.
     *   When undefined, all registered domains are captured.
     * @returns StateSnapshot.
     */
    getSnapshot(domainFilter) {
        const targetDomains = this._filterDomains(domainFilter);
        const domains = targetDomains.map((domain) => ({
            domain: domain.name,
            revision: domain.getRevision(),
            lastUpdatedAt: domain.getLastUpdatedAt(),
            state: serializeSafe(domain.getState()),
        }));
        return {
            capturedAt: Date.now(),
            domains,
            domainCount: domains.length,
            domainFilter,
        };
    }
    // ── Transition history API ───────────────────────────────────────────────────
    /**
     * Poll for new transitions and record any detected domain revisions.
     *
     * Call this periodically (e.g. after state mutations) to keep the
     * transition log up to date.
     *
     * @returns Number of new transitions recorded.
     */
    poll() {
        let recorded = 0;
        const now = Date.now();
        for (const domain of this._domains) {
            if (this._observedDomains && !this._observedDomains.has(domain.name))
                continue;
            const currentRevision = domain.getRevision();
            const cached = this._domainCache.get(domain.name);
            if (!cached || cached.revision !== currentRevision) {
                const fromRevision = cached?.revision ?? 0;
                const rawState = domain.getState();
                // Attempt to extract 'source' from the domain state
                const source = typeof rawState['source'] === 'string'
                    ? rawState['source']
                    : 'unknown';
                const serializedState = serializeSafe(rawState);
                const entry = this._transitionLog.append({
                    domain: domain.name,
                    fromRevision,
                    toRevision: currentRevision,
                    recordedAt: now,
                    source,
                    state: serializedState,
                });
                // Record timeline event for time-travel replay
                this._timeline.append({
                    capturedAt: now,
                    domain: domain.name,
                    transitionId: entry.id,
                    snapshot: serializedState,
                    label: source !== 'unknown' ? source : undefined,
                });
                this._domainCache.set(domain.name, {
                    revision: currentRevision,
                    lastUpdatedAt: domain.getLastUpdatedAt(),
                    source,
                });
                recorded++;
            }
        }
        if (recorded > 0)
            this._notifyAll();
        return recorded;
    }
    /**
     * Return all retained transition entries in chronological order.
     *
     * @returns Ordered array of TransitionEntry.
     */
    getTransitionHistory() {
        return this._transitionLog.getAll();
    }
    /**
     * Return transition history filtered by domain.
     *
     * @param domain - Domain name to filter by.
     */
    getTransitionsByDomain(domain) {
        return this._transitionLog.getByDomain(domain);
    }
    /**
     * Return transitions recorded at or after the given epoch ms timestamp.
     *
     * @param sinceMs - Inclusive lower bound (epoch ms).
     */
    getTransitionsSince(sinceMs) {
        return this._transitionLog.getSince(sinceMs);
    }
    /**
     * Return the N most recent transitions.
     *
     * @param n - Maximum number of entries.
     */
    getLastTransitions(n) {
        return this._transitionLog.getLast(n);
    }
    /** Total number of transitions ever recorded (not capped by maxTransitions). */
    get totalTransitions() {
        return this._transitionLog.totalAppended;
    }
    /**
     * Clear all stored transitions and timeline events.
     * Does not reset subscription registry or domain cache.
     */
    clearTransitionHistory() {
        this._transitionLog.clear();
        this._timeline.clear();
    }
    // ── Time-travel API ──────────────────────────────────────────────────────────
    /**
     * Return all retained timeline events in chronological order.
     *
     * @returns Ordered array of TimelineEvent.
     */
    getTimeline() {
        return this._timeline.getAll();
    }
    /**
     * Return the event at the current time-travel cursor position.
     * Returns undefined when the cursor is at the live position.
     *
     * @returns TimelineEvent or undefined when live.
     */
    getCurrentTimelineEvent() {
        return this._timeline.getCurrentEvent();
    }
    /**
     * Current time-travel cursor state.
     */
    get timeTravelCursor() {
        return this._timeline.cursorState;
    }
    /**
     * Whether the inspector is currently in time-travel mode (cursor pinned).
     */
    get isTimeTravel() {
        return !this._timeline.isLive;
    }
    /**
     * Step the cursor one event backward (toward oldest).
     *
     * @returns true if the cursor moved.
     */
    stepBack() {
        return this._timeline.stepBack();
    }
    /**
     * Step the cursor one event forward (toward live).
     *
     * @returns true if the cursor moved.
     */
    stepForward() {
        return this._timeline.stepForward();
    }
    /**
     * Seek the cursor to an absolute logical index.
     * Pass `timeline.size` to return to live.
     *
     * @param index — Target index (size = live).
     */
    seekTo(index) {
        this._timeline.seekTo(index);
    }
    /**
     * Seek to the nearest event at or before a given epoch ms timestamp.
     *
     * @param epochMs — Target timestamp.
     */
    seekToTime(epochMs) {
        this._timeline.seekToTime(epochMs);
    }
    /**
     * Exit time-travel mode, returning the cursor to the live tail.
     */
    exitTimeTravel() {
        this._timeline.exitTimeTravel();
    }
    /**
     * Get the snapshot at the current cursor position for display.
     * Returns undefined when live (callers should use getSnapshot() instead).
     *
     * @returns The pinned snapshot state or undefined when live.
     */
    getTimeTravelSnapshot() {
        return this._timeline.getCurrentEvent();
    }
    // ── Subscription API ─────────────────────────────────────────────────────────
    /**
     * Subscribe to state inspector change notifications.
     *
     * @param callback - Function invoked when a domain transition is detected.
     * @param label - Human-readable label for this subscriber.
     * @param domainFilter - Optional domain names to restrict notifications to.
     * @returns Object with `id` (subscription ID) and `unsubscribe` function.
     */
    subscribe(callback, label, domainFilter) {
        const id = `sub-${++this._subIdCounter}`;
        const info = {
            id,
            label,
            registeredAt: Date.now(),
            domainFilter,
            notificationCount: 0,
            lastNotifiedAt: undefined,
            callback,
            errorCount: 0,
            lastError: undefined,
        };
        this._subscriptions.set(id, info);
        return {
            id,
            unsubscribe: () => this._subscriptions.delete(id),
        };
    }
    /**
     * Return metadata for all active subscriptions.
     * Callbacks are not exposed.
     *
     * @returns Array of SubscriptionInfo.
     */
    getSubscriptions() {
        return [...this._subscriptions.values()].map((sub) => ({
            id: sub.id,
            label: sub.label,
            registeredAt: sub.registeredAt,
            domainFilter: sub.domainFilter,
            notificationCount: sub.notificationCount,
            lastNotifiedAt: sub.lastNotifiedAt,
            errorCount: sub.errorCount,
            lastError: sub.lastError,
        }));
    }
    /**
     * Return the count of currently active subscriptions.
     */
    get subscriptionCount() {
        return this._subscriptions.size;
    }
    // ── Private helpers ──────────────────────────────────────────────────────────
    /** Filter domains by the provided list, or return all if undefined. */
    _filterDomains(filter) {
        if (!filter)
            return this._domains;
        const filterSet = new Set(filter);
        return this._domains.filter((d) => filterSet.has(d.name));
    }
    /** Notify all subscribers, tracking notification metadata. */
    _notifyAll() {
        const now = Date.now();
        for (const sub of this._subscriptions.values()) {
            try {
                sub.callback();
                sub.notificationCount++;
                sub.lastNotifiedAt = now;
            }
            catch (err) {
                // Non-fatal: subscriber errors must not crash the provider
                sub.errorCount = (sub.errorCount ?? 0) + 1;
                sub.lastError = summarizeError(err);
            }
        }
    }
}
