import { LixObservable } from "./lix-observable.js";
import { determineSchemaKeys, extractLiteralFilters, } from "./determine-schema-keys.js";
import { LixSchemaViewMap } from "../database/schema-view-map.js";
const entityViewSchemaKeys = new Set(Object.values(LixSchemaViewMap).map((schema) => schema["x-lix-key"]));
/**
 * Detects merge commits from state commit payloads.
 *
 * Returns version IDs whose latest tip references a commit with multiple parent edges.
 */
function detectMergedVersions(changes) {
    const commitsWithMultipleParents = new Set();
    for (const change of changes) {
        if (change.schema_key !== "lix_commit")
            continue;
        const snapshot = change.snapshot_content;
        const parentIds = Array.isArray(snapshot?.parent_commit_ids)
            ? snapshot.parent_commit_ids.filter((parent) => typeof parent === "string" && parent.length > 0)
            : [];
        if (parentIds.length >= 2) {
            commitsWithMultipleParents.add(change.entity_id);
        }
    }
    if (commitsWithMultipleParents.size === 0) {
        return new Set();
    }
    const mergedVersionIds = new Set();
    for (const change of changes) {
        if (change.schema_key !== "lix_version_tip")
            continue;
        const commitId = change.snapshot_content?.commit_id;
        if (typeof commitId === "string" &&
            commitsWithMultipleParents.has(commitId)) {
            mergedVersionIds.add(change.entity_id);
        }
    }
    return mergedVersionIds;
}
/**
 * Deep equality check for query results.
 * Compares arrays element by element using JSON.stringify for simplicity.
 */
function areResultsEqual(a, b) {
    if (a.length !== b.length)
        return false;
    // Simple deep equality using JSON.stringify
    // This works well for database results which are typically JSON-serializable
    return JSON.stringify(a) === JSON.stringify(b);
}
/**
 * Creates the observe function for a Lix instance.
 *
 * @param lix - The Lix instance to add observables to
 * @returns The observe function
 */
export function createObserve(lix) {
    const activeObservables = new WeakMap();
    // Pool observables per compiled SQL + params + mode so identical observe()
    // calls share a single upstream execution and state-commit subscription.
    const pool = new Map();
    /**
     * Converts a Kysely read-query into a LixObservable – an implementation of
     * the TC-39 Observable protocol that emits a fresh result-set whenever the
     * underlying state mutates.
     *
     * Pooling:
     * - Identical queries (same compiled SQL + parameters + mode) on the same Lix instance
     *   return the same observable instance and share a single upstream execution and
     *   state-commit listener.
     * - Pooling is scoped per Lix instance. Different Lix instances never share pooling.
     * - Different modes ("array" vs "first"/"firstOrThrow") do not pool with each other.
     * - The pooled observable remains active until the last subscriber unsubscribes; then
     *   the upstream is torn down and the pool entry is evicted.
     *
     * @example Full table stream
     * ```ts
     * const obs = observe(lix.db.selectFrom('key_value').selectAll())
     * const a = obs.subscribe({ next: rows => console.log('A', rows.length) })
     * const b = obs.subscribe({ next: rows => console.log('B', rows.length) })
     * // Both subscribers share one upstream execution
     * a.unsubscribe(); b.unsubscribe();
     * ```
     *
     * @example Watch latest change-set (first row)
     * ```ts
     * observe(
     *   lix.db.selectFrom('change_set_by_version')
     *         .selectAll()
     *         .orderBy('created_at desc')
     * ).subscribeTakeFirst({ next: cs => console.log('head →', cs) })
     * ```
     */
    return function observe(query, options = { mode: "array" }) {
        // Build a stable pooling key based on compiled SQL, parameters and mode
        const compiled = query.compile();
        const mode = options.mode ?? "array";
        const poolKey = `${mode}:${compiled.sql}:${JSON.stringify(compiled.parameters)}`;
        // If an observable already exists for this key, return it
        const existing = pool.get(poolKey);
        if (existing)
            return existing;
        const listeners = new Set();
        let isActive = false;
        let previousResult;
        let unsubscribeFromStateCommit;
        const notifyNext = (rows) => {
            for (const l of listeners)
                l.next?.(rows);
        };
        const notifyError = (err) => {
            for (const l of listeners)
                l.error?.(err);
        };
        // Helper to execute the query
        const executeQuery = async () => {
            if (!isActive)
                return;
            try {
                // Optimize query for first/firstOrThrow modes when executing
                let optimizedQuery = query;
                if (mode === "first" || mode === "firstOrThrow") {
                    optimizedQuery = query.limit(1);
                }
                const result = await optimizedQuery.execute();
                if (!isActive)
                    return;
                const rows = result;
                const changed = !previousResult || !areResultsEqual(previousResult, rows);
                if (changed) {
                    previousResult = rows;
                    notifyNext(rows);
                }
            }
            catch (error) {
                if (!isActive)
                    return;
                notifyError(error);
            }
        };
        const shouldReexecute = (data) => {
            // Extract changes from the data structure
            const changes = data.changes || [];
            // If no changes provided, always re-execute for safety
            if (!changes || changes.length === 0) {
                return true;
            }
            // Get schema keys that this query depends on
            const recompiled = query.compile();
            const schemaKeys = determineSchemaKeys(recompiled);
            const filters = extractLiteralFilters(recompiled);
            const watchedVersions = new Set(filters.versionIds);
            const watchedEntityIds = new Set(filters.entityIds);
            const watchedFileIds = new Set(filters.fileIds);
            const targetsFileTable = schemaKeys.includes("lix_file_descriptor");
            const previousFileIds = targetsFileTable && previousResult
                ? new Set(previousResult
                    .map((row) => row?.file_id ?? row?.id)
                    .filter((id) => typeof id === "string"))
                : new Set();
            const targetsActiveVersion = watchedVersions.size === 0 &&
                (schemaKeys.includes("state") ||
                    schemaKeys.some((key) => entityViewSchemaKeys.has(key)));
            // If no schema keys extracted, always re-execute for safety
            if (!schemaKeys.length) {
                return true;
            }
            // Detect merge commits (identified by commits with two parent edges)
            // and force re-execution for queries targeting the affected versions.
            const mergedVersionIds = detectMergedVersions(changes);
            if (mergedVersionIds.size > 0) {
                if (watchedVersions.size === 0) {
                    return true;
                }
                for (const vid of mergedVersionIds) {
                    if (watchedVersions.has(vid)) {
                        return true;
                    }
                }
            }
            // Active version flips must re-run any query using the active 'state' view.
            if (targetsActiveVersion &&
                changes.some((change) => change.schema_key === "lix_active_version")) {
                return true;
            }
            // Check if any of the changed entities match our query's schema keys
            return changes.some((change) => {
                // changesToRealize is an array of arrays: [change_id, entity_id, schema_key, ...]
                const schemaKey = change[2] || change.schema_key;
                const changeVid = change.lixcol_version_id ||
                    change.version_id;
                const changeFileId = change.file_id;
                // File reads depend on all schema keys touching the same file id.
                if (targetsFileTable && changeFileId) {
                    if (watchedFileIds.size > 0 && watchedFileIds.has(changeFileId)) {
                        return true;
                    }
                    if (watchedFileIds.size === 0 &&
                        previousFileIds.size > 0 &&
                        previousFileIds.has(changeFileId)) {
                        return true;
                    }
                }
                // Special case: queries with 'change' schema should always re-execute
                if (schemaKeys.includes("change")) {
                    return true;
                }
                // Check if this specific schema key affects our query
                if (!schemaKeys.includes(schemaKey))
                    return false;
                // If the query watches a specific version_id and the change carries a version id,
                // re-exec only when they match. If the change has no version id info, be conservative.
                if (watchedVersions.size > 0) {
                    if (changeVid && !watchedVersions.has(changeVid))
                        return false;
                }
                if (watchedEntityIds.size > 0) {
                    if (change.entity_id) {
                        if (!watchedEntityIds.has(change.entity_id))
                            return false;
                    }
                    else {
                        return true; // missing entity info → be conservative
                    }
                }
                return true;
            });
        };
        const observable = new LixObservable((observer) => {
            // Add observer to pool
            listeners.add(observer);
            // Start upstream on first subscriber
            if (!isActive) {
                isActive = true;
                // Execute initial query and subscribe for updates
                void executeQuery();
                unsubscribeFromStateCommit = lix.hooks.onStateCommit((data) => {
                    if (shouldReexecute(data))
                        void executeQuery();
                });
            }
            // If we already have a cached result, emit it immediately
            if (previousResult)
                observer.next?.(previousResult);
            // Cleanup for this subscriber
            return () => {
                listeners.delete(observer);
                if (listeners.size === 0) {
                    // Tear down upstream and remove from pool when last unsubscribes
                    isActive = false;
                    unsubscribeFromStateCommit?.();
                    pool.delete(poolKey);
                }
            };
        });
        // Track cleanup for non-pooled APIs that may reference activeObservables
        activeObservables.set(observable, {
            unsubscribeFromStateCommit: () => unsubscribeFromStateCommit?.(),
        });
        // Store in pool and return
        pool.set(poolKey, observable);
        return observable;
    };
}
//# sourceMappingURL=create-observe.js.map