import { D2, output, serializeValue } from "@tanstack/db-ivm";
import { compileQuery, INCLUDES_ROUTING, FN_SELECT_STATE } from "../compiler/index.js";
import { createCollection } from "../../collection/index.js";
import { SetWindowRequiresOrderByError, MissingAliasInputsError } from "../../errors.js";
import { transactionScopedScheduler } from "../../scheduler.js";
import { getActiveTransaction } from "../../transactions.js";
import { CollectionSubscriber } from "./collection-subscriber.js";
import { getCollectionBuilder } from "./collection-registry.js";
import { LIVE_QUERY_INTERNAL } from "./internal.js";
import { buildQueryFromConfig, extractCollectionsFromQuery, extractCollectionAliases, extractCollectionFromSource } from "./utils.js";
let liveQueryCollectionCounter = 0;
class CollectionConfigBuilder {
  constructor(config) {
    this.config = config;
    this.compiledAliasToCollectionId = {};
    this.resultKeys = /* @__PURE__ */ new WeakMap();
    this.orderByIndices = /* @__PURE__ */ new WeakMap();
    this.isGraphRunning = false;
    this.runCount = 0;
    this.isInErrorState = false;
    this.aliasDependencies = {};
    this.builderDependencies = /* @__PURE__ */ new Set();
    this.pendingGraphRuns = /* @__PURE__ */ new Map();
    this.subscriptions = {};
    this.lazySourcesCallbacks = {};
    this.lazySources = /* @__PURE__ */ new Set();
    this.optimizableOrderByCollections = {};
    this.id = config.id || `live-query-${++liveQueryCollectionCounter}`;
    this.query = buildQueryFromConfig({
      query: config.query,
      requireObjectResult: true
    });
    this.collections = extractCollectionsFromQuery(this.query);
    const collectionAliasesById = extractCollectionAliases(this.query);
    this.collectionByAlias = {};
    for (const [collectionId, aliases] of collectionAliasesById.entries()) {
      const collection = this.collections[collectionId];
      if (!collection) continue;
      for (const alias of aliases) {
        this.collectionByAlias[alias] = collection;
      }
    }
    if (this.query.orderBy && this.query.orderBy.length > 0) {
      this.compare = createOrderByComparator(this.orderByIndices);
    }
    this.compareOptions = this.config.defaultStringCollation ?? extractCollectionFromSource(this.query).compareOptions;
    this.compileBasePipeline();
  }
  /**
   * Recursively checks if a query or any of its subqueries contains joins
   */
  hasJoins(query) {
    if (query.join && query.join.length > 0) {
      return true;
    }
    if (query.from.type === `queryRef`) {
      if (this.hasJoins(query.from.query)) {
        return true;
      }
    } else if (query.from.type === `unionFrom`) {
      for (const source of query.from.sources) {
        if (source.type === `queryRef` && this.hasJoins(source.query)) {
          return true;
        }
      }
    } else if (query.from.type === `unionAll`) {
      for (const branch of query.from.queries) {
        if (this.hasJoins(branch)) {
          return true;
        }
      }
    }
    return false;
  }
  getConfig() {
    return {
      id: this.id,
      getKey: this.config.getKey || ((item) => this.resultKeys.get(item) ?? item.$key),
      sync: this.getSyncConfig(),
      compare: this.compare,
      defaultStringCollation: this.compareOptions,
      gcTime: this.config.gcTime ?? 5e3,
      // 5 seconds by default for live queries
      schema: this.config.schema,
      onInsert: this.config.onInsert,
      onUpdate: this.config.onUpdate,
      onDelete: this.config.onDelete,
      startSync: this.config.startSync,
      singleResult: this.query.singleResult,
      utils: {
        getRunCount: this.getRunCount.bind(this),
        setWindow: this.setWindow.bind(this),
        getWindow: this.getWindow.bind(this),
        [LIVE_QUERY_INTERNAL]: {
          getBuilder: () => this,
          hasCustomGetKey: !!this.config.getKey,
          hasJoins: this.hasJoins(this.query),
          hasDistinct: !!this.query.distinct
        }
      }
    };
  }
  setWindow(options) {
    if (!this.windowFn) {
      throw new SetWindowRequiresOrderByError();
    }
    this.currentWindow = options;
    this.windowFn(options);
    this.maybeRunGraphFn?.();
    if (this.liveQueryCollection?.isLoadingSubset) {
      return new Promise((resolve) => {
        const unsubscribe = this.liveQueryCollection.on(
          `loadingSubset:change`,
          (event) => {
            if (!event.isLoadingSubset) {
              unsubscribe();
              resolve();
            }
          }
        );
      });
    }
    return true;
  }
  getWindow() {
    if (!this.windowFn || !this.currentWindow) {
      return void 0;
    }
    return {
      offset: this.currentWindow.offset ?? 0,
      limit: this.currentWindow.limit ?? 0
    };
  }
  /**
   * Resolves a collection alias to its collection ID.
   *
   * Uses a two-tier lookup strategy:
   * 1. First checks compiled aliases (includes subquery inner aliases)
   * 2. Falls back to declared aliases from the query's from/join clauses
   *
   * @param alias - The alias to resolve (e.g., "employee", "manager")
   * @returns The collection ID that the alias references
   * @throws {Error} If the alias is not found in either lookup
   */
  getCollectionIdForAlias(alias) {
    const compiled = this.compiledAliasToCollectionId[alias];
    if (compiled) {
      return compiled;
    }
    const collection = this.collectionByAlias[alias];
    if (collection) {
      return collection.id;
    }
    throw new Error(`Unknown source alias "${alias}"`);
  }
  isLazyAlias(alias) {
    return this.lazySources.has(alias);
  }
  // The callback function is called after the graph has run.
  // This gives the callback a chance to load more data if needed,
  // that's used to optimize orderBy operators that set a limit,
  // in order to load some more data if we still don't have enough rows after the pipeline has run.
  // That can happen because even though we load N rows, the pipeline might filter some of these rows out
  // causing the orderBy operator to receive less than N rows or even no rows at all.
  // So this callback would notice that it doesn't have enough rows and load some more.
  // The callback returns a boolean, when it's true it's done loading data and we can mark the collection as ready.
  maybeRunGraph(callback) {
    if (this.isGraphRunning) {
      return;
    }
    if (!this.currentSyncConfig || !this.currentSyncState) {
      throw new Error(
        `maybeRunGraph called without active sync session. This should not happen.`
      );
    }
    this.isGraphRunning = true;
    try {
      const { begin, commit } = this.currentSyncConfig;
      const syncState = this.currentSyncState;
      if (this.isInErrorState) {
        return;
      }
      if (syncState.subscribedToAllCollections) {
        let callbackCalled = false;
        while (syncState.graph.pendingWork()) {
          syncState.graph.run();
          syncState.flushPendingChanges?.();
          callback?.();
          callbackCalled = true;
        }
        if (!callbackCalled) {
          callback?.();
        }
        if (syncState.messagesCount === 0) {
          begin();
          commit();
        }
        this.updateLiveQueryStatus(this.currentSyncConfig);
      }
    } finally {
      this.isGraphRunning = false;
    }
  }
  /**
   * Schedules a graph run with the transaction-scoped scheduler.
   * Ensures each builder runs at most once per transaction, with automatic dependency tracking
   * to run parent queries before child queries. Outside a transaction, runs immediately.
   *
   * Multiple calls during a transaction are coalesced into a single execution.
   * Dependencies are auto-discovered from subscribed live queries, or can be overridden.
   * Load callbacks are combined when entries merge.
   *
   * Uses the current sync session's config and syncState from instance properties.
   *
   * @param callback - Optional callback to load more data if needed (returns true when done)
   * @param options - Optional scheduling configuration
   * @param options.contextId - Transaction ID to group work; defaults to active transaction
   * @param options.jobId - Unique identifier for this job; defaults to this builder instance
   * @param options.alias - Source alias that triggered this schedule; adds alias-specific dependencies
   * @param options.dependencies - Explicit dependency list; overrides auto-discovered dependencies
   */
  scheduleGraphRun(callback, options) {
    const contextId = options?.contextId ?? getActiveTransaction()?.id;
    const jobId = options?.jobId ?? this;
    const dependentBuilders = (() => {
      if (options?.dependencies) {
        return options.dependencies;
      }
      const deps = new Set(this.builderDependencies);
      if (options?.alias) {
        const aliasDeps = this.aliasDependencies[options.alias];
        if (aliasDeps) {
          for (const dep of aliasDeps) {
            deps.add(dep);
          }
        }
      }
      deps.delete(this);
      return Array.from(deps);
    })();
    if (contextId) {
      for (const dep of dependentBuilders) {
        if (typeof dep.scheduleGraphRun === `function`) {
          dep.scheduleGraphRun(void 0, { contextId });
        }
      }
    }
    if (!this.currentSyncConfig || !this.currentSyncState) {
      throw new Error(
        `scheduleGraphRun called without active sync session. This should not happen.`
      );
    }
    let pending = contextId ? this.pendingGraphRuns.get(contextId) : void 0;
    if (!pending) {
      pending = {
        loadCallbacks: /* @__PURE__ */ new Set()
      };
      if (contextId) {
        this.pendingGraphRuns.set(contextId, pending);
      }
    }
    if (callback) {
      pending.loadCallbacks.add(callback);
    }
    const pendingToPass = contextId ? void 0 : pending;
    transactionScopedScheduler.schedule({
      contextId,
      jobId,
      dependencies: dependentBuilders,
      run: () => this.executeGraphRun(contextId, pendingToPass)
    });
  }
  /**
   * Clears pending graph run state for a specific context.
   * Called when the scheduler clears a context (e.g., transaction rollback/abort).
   */
  clearPendingGraphRun(contextId) {
    this.pendingGraphRuns.delete(contextId);
  }
  /**
   * Returns true if this builder has a pending graph run for the given context.
   */
  hasPendingGraphRun(contextId) {
    return this.pendingGraphRuns.has(contextId);
  }
  /**
   * Executes a pending graph run. Called by the scheduler when dependencies are satisfied.
   * Clears the pending state BEFORE execution so that any re-schedules during the run
   * create fresh state and don't interfere with the current execution.
   * Uses instance sync state - if sync has ended, gracefully returns without executing.
   *
   * @param contextId - Optional context ID to look up pending state
   * @param pendingParam - For immediate execution (no context), pending state is passed directly
   */
  executeGraphRun(contextId, pendingParam) {
    const pending = pendingParam ?? (contextId ? this.pendingGraphRuns.get(contextId) : void 0);
    if (contextId) {
      this.pendingGraphRuns.delete(contextId);
    }
    if (!pending) {
      return;
    }
    if (!this.currentSyncConfig || !this.currentSyncState) {
      return;
    }
    this.incrementRunCount();
    const combinedLoader = () => {
      let allDone = true;
      let firstError;
      pending.loadCallbacks.forEach((loader) => {
        try {
          allDone = loader() && allDone;
        } catch (error) {
          allDone = false;
          firstError ??= error;
        }
      });
      if (firstError) {
        throw firstError;
      }
      return allDone;
    };
    this.maybeRunGraph(combinedLoader);
  }
  getSyncConfig() {
    return {
      rowUpdateMode: `full`,
      sync: this.syncFn.bind(this)
    };
  }
  incrementRunCount() {
    this.runCount++;
  }
  getRunCount() {
    return this.runCount;
  }
  syncFn(config) {
    this.liveQueryCollection = config.collection;
    this.isInErrorState = false;
    this.currentSyncConfig = config;
    const syncState = {
      messagesCount: 0,
      subscribedToAllCollections: false,
      unsubscribeCallbacks: /* @__PURE__ */ new Set()
    };
    const fullSyncState = this.extendPipelineWithChangeProcessing(
      config,
      syncState
    );
    this.currentSyncState = fullSyncState;
    this.unsubscribeFromSchedulerClears = transactionScopedScheduler.onClear(
      (contextId) => {
        this.clearPendingGraphRun(contextId);
      }
    );
    const loadingSubsetUnsubscribe = config.collection.on(
      `loadingSubset:change`,
      (event) => {
        if (!event.isLoadingSubset) {
          this.updateLiveQueryStatus(config);
        }
      }
    );
    syncState.unsubscribeCallbacks.add(loadingSubsetUnsubscribe);
    const loadSubsetDataCallbacks = this.subscribeToAllCollections(
      config,
      fullSyncState
    );
    this.maybeRunGraphFn = () => this.scheduleGraphRun(loadSubsetDataCallbacks);
    this.scheduleGraphRun(loadSubsetDataCallbacks);
    return () => {
      syncState.unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe());
      this.currentSyncConfig = void 0;
      this.currentSyncState = void 0;
      this.pendingGraphRuns.clear();
      this.graphCache = void 0;
      this.inputsCache = void 0;
      this.pipelineCache = void 0;
      this.sourceWhereClausesCache = void 0;
      this.includesCache = void 0;
      this.lazySources.clear();
      this.optimizableOrderByCollections = {};
      this.lazySourcesCallbacks = {};
      Object.keys(this.subscriptions).forEach(
        (key) => delete this.subscriptions[key]
      );
      this.compiledAliasToCollectionId = {};
      this.unsubscribeFromSchedulerClears?.();
      this.unsubscribeFromSchedulerClears = void 0;
    };
  }
  /**
   * Compiles the query pipeline with all declared aliases.
   */
  compileBasePipeline() {
    this.graphCache = new D2();
    this.inputsCache = Object.fromEntries(
      Object.keys(this.collectionByAlias).map((alias) => [
        alias,
        this.graphCache.newInput()
      ])
    );
    const compilation = compileQuery(
      this.query,
      this.inputsCache,
      this.collections,
      this.subscriptions,
      this.lazySourcesCallbacks,
      this.lazySources,
      this.optimizableOrderByCollections,
      (windowFn) => {
        this.windowFn = windowFn;
      }
    );
    this.pipelineCache = compilation.pipeline;
    this.sourceWhereClausesCache = compilation.sourceWhereClauses;
    this.compiledAliasToCollectionId = compilation.aliasToCollectionId;
    this.includesCache = compilation.includes;
    const missingAliases = Object.keys(this.compiledAliasToCollectionId).filter(
      (alias) => !Object.hasOwn(this.inputsCache, alias)
    );
    if (missingAliases.length > 0) {
      throw new MissingAliasInputsError(missingAliases);
    }
  }
  maybeCompileBasePipeline() {
    if (!this.graphCache || !this.inputsCache || !this.pipelineCache) {
      this.compileBasePipeline();
    }
    return {
      graph: this.graphCache,
      inputs: this.inputsCache,
      pipeline: this.pipelineCache
    };
  }
  extendPipelineWithChangeProcessing(config, syncState) {
    const { begin, commit } = config;
    const { graph, inputs, pipeline } = this.maybeCompileBasePipeline();
    let pendingChanges = /* @__PURE__ */ new Map();
    pipeline.pipe(
      output((data) => {
        const messages = data.getInner();
        syncState.messagesCount += messages.length;
        messages.reduce(accumulateChanges, pendingChanges);
      })
    );
    const includesState = this.setupIncludesOutput(
      this.includesCache,
      syncState
    );
    syncState.flushPendingChanges = () => {
      const hasParentChanges = pendingChanges.size > 0;
      const hasChildChanges = hasPendingIncludesChanges(includesState);
      if (!hasParentChanges && !hasChildChanges) {
        return;
      }
      let changesToApply = pendingChanges;
      if (this.config.getKey) {
        const merged = /* @__PURE__ */ new Map();
        for (const [, changes] of pendingChanges) {
          const customKey = this.config.getKey(changes.value);
          const existing = merged.get(customKey);
          if (existing) {
            existing.inserts += changes.inserts;
            existing.deletes += changes.deletes;
            if (changes.inserts > 0) {
              existing.value = changes.value;
              if (changes.orderByIndex !== void 0) {
                existing.orderByIndex = changes.orderByIndex;
              }
            }
          } else {
            merged.set(customKey, { ...changes });
          }
        }
        changesToApply = merged;
      }
      if (hasParentChanges) {
        begin();
        changesToApply.forEach(this.applyChanges.bind(this, config));
        commit();
      }
      pendingChanges = /* @__PURE__ */ new Map();
      flushIncludesState(
        includesState,
        config.collection,
        this.id,
        hasParentChanges ? changesToApply : null,
        config
      );
    };
    graph.finalize();
    syncState.graph = graph;
    syncState.inputs = inputs;
    syncState.pipeline = pipeline;
    return syncState;
  }
  /**
   * Sets up output callbacks for includes child pipelines.
   * Each includes entry gets its own output callback that accumulates child changes,
   * and a child registry that maps correlation key → child Collection.
   */
  setupIncludesOutput(includesEntries, syncState) {
    if (!includesEntries || includesEntries.length === 0) {
      return [];
    }
    return includesEntries.map((entry) => {
      const state = {
        fieldName: entry.fieldName,
        resultPath: entry.resultPath,
        childCorrelationField: entry.childCorrelationField,
        hasOrderBy: entry.hasOrderBy,
        materialization: entry.materialization,
        scalarField: entry.scalarField,
        childRegistry: /* @__PURE__ */ new Map(),
        pendingChildChanges: /* @__PURE__ */ new Map(),
        correlationToParentKeys: /* @__PURE__ */ new Map()
      };
      entry.pipeline.pipe(
        output((data) => {
          const messages = data.getInner();
          syncState.messagesCount += messages.length;
          for (const [[childKey, tupleData], multiplicity] of messages) {
            const [childResult, _orderByIndex, correlationKey, parentContext] = tupleData;
            const routingKey = computeRoutingKey(correlationKey, parentContext);
            let byChild = state.pendingChildChanges.get(routingKey);
            if (!byChild) {
              byChild = /* @__PURE__ */ new Map();
              state.pendingChildChanges.set(routingKey, byChild);
            }
            const existing = byChild.get(childKey) || {
              deletes: 0,
              inserts: 0,
              value: childResult,
              orderByIndex: _orderByIndex
            };
            if (multiplicity < 0) {
              existing.deletes += Math.abs(multiplicity);
            } else if (multiplicity > 0) {
              existing.inserts += multiplicity;
              existing.value = childResult;
            }
            byChild.set(childKey, existing);
          }
        })
      );
      if (entry.childCompilationResult.includes) {
        state.nestedSetups = setupNestedPipelines(
          entry.childCompilationResult.includes,
          syncState
        );
      }
      return state;
    });
  }
  applyChanges(config, changes, key) {
    const { write, collection } = config;
    const { deletes, inserts, value, orderByIndex } = changes;
    this.resultKeys.set(value, key);
    if (orderByIndex !== void 0) {
      this.orderByIndices.set(value, orderByIndex);
    }
    if (inserts && deletes === 0) {
      write({
        value,
        type: `insert`
      });
    } else if (
      // Insert & update(s) (updates are a delete & insert)
      inserts > deletes || // Just update(s) but the item is already in the collection (so
      // was inserted previously).
      inserts === deletes && collection.has(collection.getKeyFromItem(value))
    ) {
      write({
        value,
        type: `update`
      });
    } else if (deletes > 0) {
      write({
        value,
        type: `delete`
      });
    } else {
      throw new Error(
        `Could not apply changes: ${JSON.stringify(changes)}. This should never happen.`
      );
    }
  }
  /**
   * Handle status changes from source collections
   */
  handleSourceStatusChange(config, collectionId, event) {
    const { status } = event;
    if (status === `error`) {
      this.transitionToError(
        `Source collection '${collectionId}' entered error state`
      );
      return;
    }
    if (status === `cleaned-up`) {
      this.transitionToError(
        `Source collection '${collectionId}' was manually cleaned up while live query '${this.id}' depends on it. Live queries prevent automatic GC, so this was likely a manual cleanup() call.`
      );
      return;
    }
    this.updateLiveQueryStatus(config);
  }
  /**
   * Update the live query status based on source collection statuses
   */
  updateLiveQueryStatus(config) {
    const { markReady } = config;
    if (this.isInErrorState) {
      return;
    }
    const subscribedToAll = this.currentSyncState?.subscribedToAllCollections;
    const allReady = this.allCollectionsReady();
    const isLoading = this.liveQueryCollection?.isLoadingSubset;
    if (subscribedToAll && allReady && !isLoading) {
      markReady();
    }
  }
  /**
   * Transition the live query to error state
   */
  transitionToError(message) {
    this.isInErrorState = true;
    console.error(`[Live Query Error] ${message}`);
    this.liveQueryCollection?._lifecycle.setStatus(`error`);
  }
  allCollectionsReady() {
    return Object.values(this.collections).every(
      (collection) => collection.isReady()
    );
  }
  /**
   * Creates per-alias subscriptions enabling self-join support.
   * Each alias gets its own subscription with independent filters, even for the same collection.
   * Example: `{ employee: col, manager: col }` creates two separate subscriptions.
   */
  subscribeToAllCollections(config, syncState) {
    const compiledAliases = Object.entries(this.compiledAliasToCollectionId);
    if (compiledAliases.length === 0) {
      throw new Error(
        `Compiler returned no alias metadata for query '${this.id}'. This should not happen; please report.`
      );
    }
    const loaders = compiledAliases.map(([alias, collectionId]) => {
      const collection = this.collectionByAlias[alias] ?? this.collections[collectionId];
      const dependencyBuilder = getCollectionBuilder(collection);
      if (dependencyBuilder && dependencyBuilder !== this) {
        this.aliasDependencies[alias] = [dependencyBuilder];
        this.builderDependencies.add(dependencyBuilder);
      } else {
        this.aliasDependencies[alias] = [];
      }
      const collectionSubscriber = new CollectionSubscriber(
        alias,
        collectionId,
        collection,
        this
      );
      const statusUnsubscribe = collection.on(`status:change`, (event) => {
        this.handleSourceStatusChange(config, collectionId, event);
      });
      syncState.unsubscribeCallbacks.add(statusUnsubscribe);
      const subscription = collectionSubscriber.subscribe();
      this.subscriptions[alias] = subscription;
      const loadMore = collectionSubscriber.loadMoreIfNeeded.bind(
        collectionSubscriber,
        subscription
      );
      return loadMore;
    });
    const loadSubsetDataCallbacks = () => {
      loaders.map((loader) => loader());
      return true;
    };
    syncState.subscribedToAllCollections = true;
    return loadSubsetDataCallbacks;
  }
}
function createOrderByComparator(orderByIndices) {
  return (val1, val2) => {
    const index1 = orderByIndices.get(val1);
    const index2 = orderByIndices.get(val2);
    if (index1 && index2) {
      if (index1 < index2) {
        return -1;
      } else if (index1 > index2) {
        return 1;
      } else {
        return 0;
      }
    }
    return 0;
  };
}
function materializesInline(state) {
  return state.materialization !== `collection`;
}
function materializeIncludedValue(state, entry) {
  if (!entry) {
    if (state.materialization === `array`) {
      return [];
    }
    if (state.materialization === `concat`) {
      return ``;
    }
    return void 0;
  }
  if (state.materialization === `collection`) {
    return entry.collection;
  }
  const rows = [...entry.collection.toArray];
  const values = state.scalarField ? rows.map((row) => row?.[state.scalarField]) : rows;
  if (state.materialization === `array`) {
    return values;
  }
  if (state.materialization === `singleton`) {
    return values[0];
  }
  return values.map((value) => String(value ?? ``)).join(``);
}
function setupNestedPipelines(includes, syncState) {
  return includes.map((entry) => {
    const buffer = /* @__PURE__ */ new Map();
    entry.pipeline.pipe(
      output((data) => {
        const messages = data.getInner();
        syncState.messagesCount += messages.length;
        for (const [[childKey, tupleData], multiplicity] of messages) {
          const [childResult, _orderByIndex, correlationKey, parentContext] = tupleData;
          const routingKey = computeRoutingKey(correlationKey, parentContext);
          let byChild = buffer.get(routingKey);
          if (!byChild) {
            byChild = /* @__PURE__ */ new Map();
            buffer.set(routingKey, byChild);
          }
          const existing = byChild.get(childKey) || {
            deletes: 0,
            inserts: 0,
            value: childResult,
            orderByIndex: _orderByIndex
          };
          if (multiplicity < 0) {
            existing.deletes += Math.abs(multiplicity);
          } else if (multiplicity > 0) {
            existing.inserts += multiplicity;
            existing.value = childResult;
          }
          byChild.set(childKey, existing);
        }
      })
    );
    const setup = {
      compilationResult: entry,
      buffer,
      snapshot: /* @__PURE__ */ new Map(),
      routingIndex: /* @__PURE__ */ new Map(),
      routingReverseIndex: /* @__PURE__ */ new Map(),
      routingChildToNested: /* @__PURE__ */ new Map()
    };
    if (entry.childCompilationResult.includes) {
      setup.nestedSetups = setupNestedPipelines(
        entry.childCompilationResult.includes,
        syncState
      );
    }
    return setup;
  });
}
function createPerEntryIncludesStates(setups) {
  return setups.map((setup) => {
    const state = {
      fieldName: setup.compilationResult.fieldName,
      resultPath: setup.compilationResult.resultPath,
      childCorrelationField: setup.compilationResult.childCorrelationField,
      hasOrderBy: setup.compilationResult.hasOrderBy,
      materialization: setup.compilationResult.materialization,
      scalarField: setup.compilationResult.scalarField,
      childRegistry: /* @__PURE__ */ new Map(),
      pendingChildChanges: /* @__PURE__ */ new Map(),
      correlationToParentKeys: /* @__PURE__ */ new Map()
    };
    if (setup.nestedSetups) {
      state.nestedSetups = setup.nestedSetups;
    }
    return state;
  });
}
function cloneSnapshotValue(value) {
  if (value == null || typeof value !== `object`) {
    return value;
  }
  return Array.isArray(value) ? [...value] : { ...value };
}
function accumulateSnapshot(setup, nestedCorrelationKey, childChanges) {
  let snap = setup.snapshot.get(nestedCorrelationKey);
  if (!snap) {
    snap = /* @__PURE__ */ new Map();
    setup.snapshot.set(nestedCorrelationKey, snap);
  }
  for (const [childKey, changes] of childChanges) {
    let row = snap.get(childKey);
    if (!row) {
      row = {
        value: cloneSnapshotValue(changes.value),
        orderByIndex: changes.orderByIndex,
        count: 0
      };
      snap.set(childKey, row);
    }
    row.count += changes.inserts - changes.deletes;
    if (changes.inserts > 0) {
      row.value = cloneSnapshotValue(changes.value);
      if (changes.orderByIndex !== void 0) {
        row.orderByIndex = changes.orderByIndex;
      }
    }
    if (row.count <= 0) {
      snap.delete(childKey);
    }
  }
  if (snap.size === 0) {
    setup.snapshot.delete(nestedCorrelationKey);
  }
}
function seedParentFromSnapshot(state, setupIndex, parentCorrelationKey, nestedCorrelationKey) {
  const setup = state.nestedSetups[setupIndex];
  const snap = setup.snapshot.get(nestedCorrelationKey);
  if (!snap || snap.size === 0) return;
  const entry = state.childRegistry.get(parentCorrelationKey);
  if (!entry || !entry.includesStates) return;
  const entryState = entry.includesStates[setupIndex];
  let byChild = entryState.pendingChildChanges.get(nestedCorrelationKey);
  if (!byChild) {
    byChild = /* @__PURE__ */ new Map();
    entryState.pendingChildChanges.set(nestedCorrelationKey, byChild);
  }
  for (const [childKey, row] of snap) {
    if (byChild.has(childKey)) continue;
    byChild.set(childKey, {
      deletes: 0,
      inserts: row.count,
      value: cloneSnapshotValue(row.value),
      orderByIndex: row.orderByIndex
    });
  }
}
function drainNestedBuffers(state) {
  const dirtyCorrelationKeys = /* @__PURE__ */ new Set();
  if (!state.nestedSetups) return dirtyCorrelationKeys;
  for (const setup of state.nestedSetups) {
    const toDelete = [];
    for (const [nestedCorrelationKey, childChanges] of setup.buffer) {
      const stateRoutes = setup.routingIndex.get(nestedCorrelationKey);
      if (stateRoutes === void 0 || stateRoutes.size === 0) {
        continue;
      }
      let routedToAny = false;
      for (const [targetState, parentRoutes] of stateRoutes) {
        const targetSetupIndex = targetState.nestedSetups?.indexOf(setup) ?? -1;
        if (targetSetupIndex < 0) continue;
        for (const parentCorrelationKey of parentRoutes.keys()) {
          const entry = targetState.childRegistry.get(parentCorrelationKey);
          if (!entry || !entry.includesStates) {
            continue;
          }
          const entryState = entry.includesStates[targetSetupIndex];
          for (const [childKey, changes] of childChanges) {
            let byChild = entryState.pendingChildChanges.get(nestedCorrelationKey);
            if (!byChild) {
              byChild = /* @__PURE__ */ new Map();
              entryState.pendingChildChanges.set(nestedCorrelationKey, byChild);
            }
            const existing = byChild.get(childKey);
            if (existing) {
              existing.inserts += changes.inserts;
              existing.deletes += changes.deletes;
              if (changes.inserts > 0) {
                existing.value = changes.value;
                if (changes.orderByIndex !== void 0) {
                  existing.orderByIndex = changes.orderByIndex;
                }
              }
            } else {
              byChild.set(childKey, { ...changes });
            }
          }
          if (targetState === state) {
            dirtyCorrelationKeys.add(parentCorrelationKey);
          }
          routedToAny = true;
        }
      }
      if (routedToAny) {
        accumulateSnapshot(setup, nestedCorrelationKey, childChanges);
        toDelete.push(nestedCorrelationKey);
      }
    }
    for (const key of toDelete) {
      setup.buffer.delete(key);
    }
  }
  return dirtyCorrelationKeys;
}
function removeChildKeyFromRoute(setup, state, correlationKey, nestedRoutingKey, childKey) {
  const stateRoutes = setup.routingIndex.get(nestedRoutingKey);
  const parents = stateRoutes?.get(state);
  const childKeys = parents?.get(correlationKey);
  if (!parents || !childKeys) return;
  childKeys.delete(childKey);
  if (childKeys.size === 0) {
    parents.delete(correlationKey);
    if (parents.size === 0) {
      stateRoutes.delete(state);
      if (stateRoutes.size === 0) {
        setup.routingIndex.delete(nestedRoutingKey);
      }
    }
    const reverse = setup.routingReverseIndex.get(state);
    const reverseSet = reverse?.get(correlationKey);
    if (reverseSet) {
      reverseSet.delete(nestedRoutingKey);
      if (reverseSet.size === 0) {
        reverse.delete(correlationKey);
        if (reverse.size === 0) {
          setup.routingReverseIndex.delete(state);
        }
      }
    }
  }
}
function updateRoutingIndex(state, correlationKey, childChanges) {
  if (!state.nestedSetups) return;
  for (let i = 0; i < state.nestedSetups.length; i++) {
    const setup = state.nestedSetups[i];
    let childToNested = setup.routingChildToNested.get(state);
    if (!childToNested) {
      childToNested = /* @__PURE__ */ new Map();
      setup.routingChildToNested.set(state, childToNested);
    }
    for (const [childKey, change] of childChanges) {
      if (change.inserts > 0) {
        const nestedRouting = change.value[INCLUDES_ROUTING]?.[setup.compilationResult.fieldName];
        const nestedCorrelationKey = nestedRouting?.correlationKey;
        const nestedParentContext = nestedRouting?.parentContext ?? null;
        const nestedRoutingKey = computeRoutingKey(
          nestedCorrelationKey,
          nestedParentContext
        );
        const perParent = childToNested.get(correlationKey);
        const prevNestedKey = perParent?.get(childKey);
        if (prevNestedKey !== void 0 && prevNestedKey !== nestedRoutingKey) {
          removeChildKeyFromRoute(
            setup,
            state,
            correlationKey,
            prevNestedKey,
            childKey
          );
          perParent.delete(childKey);
        }
        if (nestedCorrelationKey != null) {
          let stateRoutes = setup.routingIndex.get(nestedRoutingKey);
          if (!stateRoutes) {
            stateRoutes = /* @__PURE__ */ new Map();
            setup.routingIndex.set(nestedRoutingKey, stateRoutes);
          }
          let parents = stateRoutes.get(state);
          if (!parents) {
            parents = /* @__PURE__ */ new Map();
            stateRoutes.set(state, parents);
          }
          let childKeys = parents.get(correlationKey);
          const isNewParent = !childKeys || childKeys.size === 0;
          if (!childKeys) {
            childKeys = /* @__PURE__ */ new Set();
            parents.set(correlationKey, childKeys);
          }
          childKeys.add(childKey);
          let reverse = setup.routingReverseIndex.get(state);
          if (!reverse) {
            reverse = /* @__PURE__ */ new Map();
            setup.routingReverseIndex.set(state, reverse);
          }
          let reverseSet = reverse.get(correlationKey);
          if (!reverseSet) {
            reverseSet = /* @__PURE__ */ new Set();
            reverse.set(correlationKey, reverseSet);
          }
          reverseSet.add(nestedRoutingKey);
          let recorded = perParent;
          if (!recorded) {
            recorded = /* @__PURE__ */ new Map();
            childToNested.set(correlationKey, recorded);
          }
          recorded.set(childKey, nestedRoutingKey);
          if (isNewParent) {
            seedParentFromSnapshot(state, i, correlationKey, nestedRoutingKey);
          }
        } else if (perParent && perParent.size === 0) {
          childToNested.delete(correlationKey);
        }
      } else if (change.deletes > 0 && change.inserts === 0) {
        const nestedRouting2 = change.value[INCLUDES_ROUTING]?.[setup.compilationResult.fieldName];
        const nestedCorrelationKey = nestedRouting2?.correlationKey;
        const nestedParentContext2 = nestedRouting2?.parentContext ?? null;
        const nestedRoutingKey = computeRoutingKey(
          nestedCorrelationKey,
          nestedParentContext2
        );
        if (nestedCorrelationKey != null) {
          removeChildKeyFromRoute(
            setup,
            state,
            correlationKey,
            nestedRoutingKey,
            childKey
          );
        }
        const perParent = childToNested.get(correlationKey);
        if (perParent) {
          perParent.delete(childKey);
          if (perParent.size === 0) childToNested.delete(correlationKey);
        }
      }
    }
  }
}
function cleanRoutingIndexOnDelete(state, correlationKey) {
  if (!state.nestedSetups) return;
  for (const setup of state.nestedSetups) {
    const reverseIndex = setup.routingReverseIndex.get(state);
    const nestedKeys = reverseIndex?.get(correlationKey);
    if (!nestedKeys) continue;
    for (const nestedKey of nestedKeys) {
      const stateRoutes = setup.routingIndex.get(nestedKey);
      const parents = stateRoutes?.get(state);
      if (parents) {
        parents.delete(correlationKey);
        if (parents.size === 0) {
          stateRoutes.delete(state);
          if (stateRoutes.size === 0) {
            setup.routingIndex.delete(nestedKey);
          }
        }
      }
    }
    reverseIndex.delete(correlationKey);
    if (reverseIndex.size === 0) {
      setup.routingReverseIndex.delete(state);
    }
    const childToNested = setup.routingChildToNested.get(state);
    if (childToNested) {
      childToNested.delete(correlationKey);
      if (childToNested.size === 0) {
        setup.routingChildToNested.delete(state);
      }
    }
  }
}
function hasNestedBufferChanges(setups) {
  for (const setup of setups) {
    if (setup.buffer.size > 0) return true;
    if (setup.nestedSetups && hasNestedBufferChanges(setup.nestedSetups))
      return true;
  }
  return false;
}
function computeRoutingKey(correlationKey, parentContext) {
  if (parentContext == null) return correlationKey;
  return JSON.stringify([correlationKey, parentContext]);
}
function createChildCollectionEntry(parentId, fieldName, correlationKey, hasOrderBy, nestedSetups) {
  const resultKeys = /* @__PURE__ */ new WeakMap();
  const orderByIndices = hasOrderBy ? /* @__PURE__ */ new WeakMap() : null;
  let syncMethods = null;
  const compare = orderByIndices ? createOrderByComparator(orderByIndices) : void 0;
  const collection = createCollection({
    id: `__child-collection:${parentId}-${fieldName}-${serializeValue(correlationKey)}`,
    getKey: (item) => resultKeys.get(item),
    compare,
    sync: {
      rowUpdateMode: `full`,
      sync: (methods) => {
        syncMethods = methods;
        return () => {
          syncMethods = null;
        };
      }
    },
    startSync: true,
    gcTime: 0
  });
  const entry = {
    collection,
    get syncMethods() {
      return syncMethods;
    },
    resultKeys,
    orderByIndices
  };
  if (nestedSetups) {
    entry.includesStates = createPerEntryIncludesStates(nestedSetups);
  }
  return entry;
}
function flushIncludesState(includesState, parentCollection, parentId, parentChanges, parentSyncMethods) {
  for (const state of includesState) {
    if (parentChanges) {
      for (const [parentKey, changes] of parentChanges) {
        if (changes.inserts > 0) {
          const parentResult = changes.value;
          const routing = parentResult[INCLUDES_ROUTING]?.[state.fieldName];
          const correlationKey = routing?.correlationKey;
          const parentContext = routing?.parentContext ?? null;
          const routingKey = computeRoutingKey(correlationKey, parentContext);
          if (correlationKey != null) {
            if (!state.childRegistry.has(routingKey)) {
              const entry = createChildCollectionEntry(
                parentId,
                state.fieldName,
                routingKey,
                state.hasOrderBy,
                state.nestedSetups
              );
              state.childRegistry.set(routingKey, entry);
            }
            let parentKeys = state.correlationToParentKeys.get(routingKey);
            if (!parentKeys) {
              parentKeys = /* @__PURE__ */ new Set();
              state.correlationToParentKeys.set(routingKey, parentKeys);
            }
            parentKeys.add(parentKey);
            const childValue = materializeIncludedValue(
              state,
              state.childRegistry.get(routingKey)
            );
            setIncludedValue(parentResult, state.resultPath, childValue);
            const storedParent = parentCollection.get(parentKey);
            if (storedParent && storedParent !== parentResult) {
              setIncludedValue(storedParent, state.resultPath, childValue);
            }
          }
        }
      }
    }
    const affectedCorrelationKeys = materializesInline(state) ? new Set(state.pendingChildChanges.keys()) : null;
    const entriesWithChildChanges = /* @__PURE__ */ new Map();
    if (state.pendingChildChanges.size > 0) {
      for (const [correlationKey, childChanges] of state.pendingChildChanges) {
        let entry = state.childRegistry.get(correlationKey);
        if (!entry) {
          entry = createChildCollectionEntry(
            parentId,
            state.fieldName,
            correlationKey,
            state.hasOrderBy,
            state.nestedSetups
          );
          state.childRegistry.set(correlationKey, entry);
        }
        if (state.materialization === `collection`) {
          attachChildCollectionToParent(
            parentCollection,
            state.resultPath,
            correlationKey,
            state.correlationToParentKeys,
            entry.collection
          );
        }
        if (entry.syncMethods) {
          entry.syncMethods.begin();
          for (const [childKey, change] of childChanges) {
            entry.resultKeys.set(change.value, childKey);
            if (entry.orderByIndices && change.orderByIndex !== void 0) {
              entry.orderByIndices.set(change.value, change.orderByIndex);
            }
            const key = entry.syncMethods.collection.getKeyFromItem(
              change.value
            );
            const childAlreadyExists = entry.syncMethods.collection.has(key);
            if (change.inserts > 0 && change.deletes === 0) {
              entry.syncMethods.write({
                value: change.value,
                type: childAlreadyExists ? `update` : `insert`
              });
            } else if (change.inserts > change.deletes || change.inserts === change.deletes && childAlreadyExists) {
              entry.syncMethods.write({ value: change.value, type: `update` });
            } else if (change.deletes > 0) {
              entry.syncMethods.write({ value: change.value, type: `delete` });
            }
          }
          entry.syncMethods.commit();
        }
        updateRoutingIndex(state, correlationKey, childChanges);
        entriesWithChildChanges.set(correlationKey, { entry, childChanges });
      }
      state.pendingChildChanges.clear();
    }
    const dirtyFromBuffers = drainNestedBuffers(state);
    for (const [, { entry, childChanges }] of entriesWithChildChanges) {
      if (entry.includesStates) {
        flushIncludesState(
          entry.includesStates,
          entry.collection,
          entry.collection.id,
          childChanges,
          entry.syncMethods
        );
      }
    }
    for (const correlationKey of dirtyFromBuffers) {
      if (entriesWithChildChanges.has(correlationKey)) continue;
      const entry = state.childRegistry.get(correlationKey);
      if (entry?.includesStates) {
        flushIncludesState(
          entry.includesStates,
          entry.collection,
          entry.collection.id,
          null,
          entry.syncMethods
        );
      }
    }
    const deepBufferDirty = /* @__PURE__ */ new Set();
    if (state.nestedSetups) {
      for (const [correlationKey, entry] of state.childRegistry) {
        if (entriesWithChildChanges.has(correlationKey)) continue;
        if (dirtyFromBuffers.has(correlationKey)) continue;
        if (entry.includesStates && hasPendingIncludesChanges(entry.includesStates)) {
          flushIncludesState(
            entry.includesStates,
            entry.collection,
            entry.collection.id,
            null,
            entry.syncMethods
          );
          deepBufferDirty.add(correlationKey);
        }
      }
    }
    const inlineReEmitKeys = materializesInline(state) ? /* @__PURE__ */ new Set([
      ...affectedCorrelationKeys || [],
      ...dirtyFromBuffers,
      ...deepBufferDirty
    ]) : null;
    if (parentSyncMethods && inlineReEmitKeys && inlineReEmitKeys.size > 0) {
      const events = [];
      for (const correlationKey of inlineReEmitKeys) {
        const parentKeys = state.correlationToParentKeys.get(correlationKey);
        if (!parentKeys) continue;
        const entry = state.childRegistry.get(correlationKey);
        for (const parentKey of parentKeys) {
          const item = parentCollection.get(parentKey);
          if (item) {
            const previousValue = cloneForIncludesUpdate(item, state.resultPath);
            setIncludedValue(
              item,
              state.resultPath,
              materializeIncludedValue(state, entry)
            );
            const nextValue = cloneForIncludesUpdate(item, state.resultPath);
            events.push({
              type: `update`,
              key: parentKey,
              value: nextValue,
              previousValue
            });
          }
        }
      }
      if (events.length > 0) {
        const changesManager = parentCollection._changes;
        changesManager.emitEvents(events, true);
      }
    }
    if (parentChanges) {
      for (const [parentKey, changes] of parentChanges) {
        if (changes.deletes > 0 && changes.inserts === 0) {
          const routing = changes.value[INCLUDES_ROUTING]?.[state.fieldName];
          const correlationKey = routing?.correlationKey;
          const parentContext = routing?.parentContext ?? null;
          const routingKey = computeRoutingKey(correlationKey, parentContext);
          if (correlationKey != null) {
            const parentKeys = state.correlationToParentKeys.get(routingKey);
            if (parentKeys) {
              parentKeys.delete(parentKey);
              if (parentKeys.size === 0) {
                cleanRoutingIndexOnDelete(state, routingKey);
                state.childRegistry.delete(routingKey);
                state.correlationToParentKeys.delete(routingKey);
              }
            }
          }
        }
      }
    }
  }
  if (parentChanges) {
    for (const [, changes] of parentChanges) {
      delete changes.value[INCLUDES_ROUTING];
    }
  }
}
function hasPendingIncludesChanges(states) {
  for (const state of states) {
    if (state.pendingChildChanges.size > 0) return true;
    if (state.nestedSetups && hasNestedBufferChanges(state.nestedSetups))
      return true;
    for (const entry of state.childRegistry.values()) {
      if (entry.includesStates && hasPendingIncludesChanges(entry.includesStates))
        return true;
    }
  }
  return false;
}
function attachChildCollectionToParent(parentCollection, resultPath, correlationKey, correlationToParentKeys, childCollection) {
  const parentKeys = correlationToParentKeys.get(correlationKey);
  if (!parentKeys) return;
  for (const parentKey of parentKeys) {
    const item = parentCollection.get(parentKey);
    if (item) {
      setIncludedValue(item, resultPath, childCollection);
    }
  }
}
function setIncludedValue(target, path, value) {
  const state = getFnSelectState(target);
  if (!state) {
    setNestedValue(target, path, value);
    return;
  }
  setNestedValue(state.sourceRow, path, value);
  refreshFnSelectResult(target, state);
}
function getFnSelectState(target) {
  return target[FN_SELECT_STATE];
}
function refreshFnSelectResult(target, state) {
  const targetRecord = target;
  const sourceRecord = state.sourceRow;
  const routing = targetRecord[INCLUDES_ROUTING] ?? sourceRecord[INCLUDES_ROUTING];
  const nextValue = state.fnSelect(state.sourceRow);
  if (!nextValue || typeof nextValue !== `object`) {
    return;
  }
  for (const key of Object.keys(target)) {
    delete target[key];
  }
  Object.assign(target, nextValue);
  if (routing) {
    targetRecord[INCLUDES_ROUTING] = routing;
  }
  Object.defineProperty(target, FN_SELECT_STATE, {
    value: state,
    enumerable: true,
    configurable: true
  });
}
function setNestedValue(target, path, value) {
  if (path.length === 0) {
    return;
  }
  let cursor = target;
  for (let i = 0; i < path.length - 1; i++) {
    const segment = path[i];
    const next = cursor[segment];
    if (next == null || typeof next !== `object`) {
      cursor[segment] = {};
    }
    cursor = cursor[segment];
  }
  cursor[path[path.length - 1]] = value;
}
function cloneForIncludesUpdate(target, path) {
  return getFnSelectState(target) ? { ...target } : clonePathForUpdate(target, path);
}
function clonePathForUpdate(target, path) {
  const root = { ...target };
  let sourceCursor = target;
  let cloneCursor = root;
  for (let i = 0; i < path.length - 1; i++) {
    const segment = path[i];
    const sourceValue = sourceCursor?.[segment];
    if (sourceValue == null || typeof sourceValue !== `object`) {
      return root;
    }
    const clonedValue = Array.isArray(sourceValue) ? [...sourceValue] : { ...sourceValue };
    cloneCursor[segment] = clonedValue;
    sourceCursor = sourceValue;
    cloneCursor = clonedValue;
  }
  return root;
}
function accumulateChanges(acc, [[key, tupleData], multiplicity]) {
  const [value, orderByIndex] = tupleData;
  const changes = acc.get(key) || {
    deletes: 0,
    inserts: 0,
    value,
    orderByIndex
  };
  if (multiplicity < 0) {
    changes.deletes += Math.abs(multiplicity);
  } else if (multiplicity > 0) {
    changes.inserts += multiplicity;
    changes.value = value;
    if (orderByIndex !== void 0) {
      changes.orderByIndex = orderByIndex;
    }
  }
  acc.set(key, changes);
  return acc;
}
export {
  CollectionConfigBuilder
};
//# sourceMappingURL=collection-config-builder.js.map
