import { isEqual, cloneDeep } from 'lodash';
import { ReplaySubject, forkJoin } from 'rxjs';
import { LoadingState } from '@grafana/schema';
import { preProcessPanelData, compareArrayValues, DataTopic, DataFrameView, rangeUtil } from '@grafana/data';
import { getRunRequest, toDataQueryError, isExpressionReference } from '@grafana/runtime';
import { SceneObjectBase } from '../core/SceneObjectBase.js';
import { sceneGraph } from '../core/sceneGraph/index.js';
import { getDataSource } from '../utils/getDataSource.js';
import { VariableDependencyConfig } from '../variables/VariableDependencyConfig.js';
import { writeSceneLog } from '../utils/writeSceneLog.js';
import { VariableValueRecorder } from '../variables/VariableValueRecorder.js';
import { emptyPanelData } from '../core/SceneDataNode.js';
import { getClosest } from '../core/sceneGraph/utils.js';
import { isExtraQueryProvider } from './ExtraQueryProvider.js';
import { extraQueryProcessingOperator, passthroughProcessor } from './extraQueryProcessingOperator.js';
import { filterAnnotations } from './layers/annotations/filterAnnotations.js';
import { getEnrichedDataRequest } from './getEnrichedDataRequest.js';
import { registerQueryWithController } from './registerQueryWithController.js';
import { findPanelProfiler } from '../utils/findPanelProfiler.js';
import { AdHocFiltersVariable } from '../variables/adhoc/AdHocFiltersVariable.js';
import { GroupByVariable } from '../variables/groupby/GroupByVariable.js';
import { DataLayersMerger } from './DataLayersMerger.js';
import { interpolate } from '../core/sceneGraph/sceneGraph.js';
import { wrapInSafeSerializableSceneObject } from '../utils/wrapInSafeSerializableSceneObject.js';
import { DrilldownDependenciesManager } from '../variables/DrilldownDependenciesManager.js';

let counter$1 = 100;
const MIXED_DATASOURCE_UID = "-- Mixed --";
const MIXED_DATASOURCE_REF = { type: "mixed", uid: MIXED_DATASOURCE_UID };
function getNextRequestId$1(prefix = "SQR") {
  return prefix + counter$1++;
}
function isTemplatedDatasourceUid(uid) {
  if (!uid) {
    return false;
  }
  return /^(?:\$[A-Za-z0-9_]+|\$\{[^}]+\})/.test(uid);
}
function isTemplatedDatasourceRef(datasource) {
  return isTemplatedDatasourceUid(datasource == null ? void 0 : datasource.uid);
}
function getSimpleVariableNameFromDatasourceUid(uid) {
  if (!uid) {
    return void 0;
  }
  const shortMatch = uid.match(/^\$([A-Za-z0-9_]+)$/);
  if (shortMatch) {
    return shortMatch[1];
  }
  const bracketMatch = uid.match(/^\$\{([A-Za-z0-9_]+)\}$/);
  if (bracketMatch) {
    return bracketMatch[1];
  }
  return void 0;
}
function getDatasourceVariableSelectionCount(sceneObject, variableName) {
  const variable = sceneGraph.lookupVariable(variableName, sceneObject);
  if (!variable) {
    return void 0;
  }
  const value = variable.getValue();
  if (Array.isArray(value)) {
    return value.filter((item) => item !== "default").length;
  }
  if (value === null || value === void 0) {
    return void 0;
  }
  return value === "default" ? 0 : 1;
}
function shouldUseMixedRuntimeDatasource(sceneObject, datasource, queries) {
  const refs = [datasource, ...queries.map((query) => query.datasource)];
  for (const ref of refs) {
    const uid = ref == null ? void 0 : ref.uid;
    if (!uid) {
      continue;
    }
    const variableName = getSimpleVariableNameFromDatasourceUid(uid);
    if (variableName) {
      const selectionCount = getDatasourceVariableSelectionCount(sceneObject, variableName);
      if (selectionCount !== void 0 && selectionCount > 1) {
        return true;
      }
      continue;
    }
  }
  return false;
}
class SceneQueryRunner extends SceneObjectBase {
  constructor(initialState) {
    super(initialState);
    this._dataLayersMerger = new DataLayersMerger();
    this._variableValueRecorder = new VariableValueRecorder();
    this._results = new ReplaySubject(1);
    this._scopedVars = { __sceneObject: wrapInSafeSerializableSceneObject(this) };
    this._isInView = true;
    this._bypassIsInView = false;
    this._queryNotExecutedWhenOutOfView = false;
    this._variableDependency = new VariableDependencyConfig(this, {
      statePaths: ["queries", "datasource", "minInterval"],
      onVariableUpdateCompleted: this.onVariableUpdatesCompleted.bind(this),
      onAnyVariableChanged: this.onAnyVariableChanged.bind(this),
      dependsOnScopes: true
    });
    this._drilldownDependenciesManager = new DrilldownDependenciesManager(this._variableDependency);
    this.onDataReceived = (data) => {
      var _a, _b, _c, _d, _e, _f, _g, _h;
      const preProcessedData = preProcessPanelData(data, this.state.data);
      this._resultAnnotations = data.annotations;
      const dataWithLayersApplied = this._combineDataLayers(preProcessedData);
      const last = this.state.data;
      if (last != null && dataWithLayersApplied.state !== LoadingState.Streaming) {
        const sameSeries = compareArrayValues((_a = last.series) != null ? _a : [], (_b = dataWithLayersApplied.series) != null ? _b : [], (a, b) => a === b);
        const sameAnnotations = compareArrayValues(
          (_c = last.annotations) != null ? _c : [],
          (_d = dataWithLayersApplied.annotations) != null ? _d : [],
          (a, b) => a === b
        );
        const sameState = last.state === dataWithLayersApplied.state;
        const sameErrors = compareArrayValues(
          (_e = last.errors) != null ? _e : [],
          (_f = dataWithLayersApplied.errors) != null ? _f : [],
          (a, b) => isEqual(a, b)
        );
        if (sameSeries) {
          dataWithLayersApplied.series = last.series;
        }
        if (sameAnnotations) {
          dataWithLayersApplied.annotations = last.annotations;
        }
        if (sameSeries && sameAnnotations && sameState && sameErrors) {
          if (((_g = last.request) == null ? void 0 : _g.requestId) === ((_h = dataWithLayersApplied.request) == null ? void 0 : _h.requestId)) {
            return;
          }
        }
      }
      let hasFetchedData = this.state._hasFetchedData;
      if (!hasFetchedData && preProcessedData.state !== LoadingState.Loading) {
        hasFetchedData = true;
      }
      this.setState({ data: dataWithLayersApplied, _hasFetchedData: hasFetchedData });
      this._results.next({ origin: this, data: dataWithLayersApplied });
    };
    this.addActivationHandler(() => this._onActivate());
  }
  getResultsStream() {
    return this._results;
  }
  _onActivate() {
    if (this.isQueryModeAuto()) {
      const timeRange = sceneGraph.getTimeRange(this);
      const providers = this.getClosestExtraQueryProviders();
      for (const provider of providers) {
        this._subs.add(
          provider.subscribeToState((n, p) => {
            if (provider.shouldRerun(p, n, this.state.queries)) {
              this.runQueries();
            }
          })
        );
      }
      this.subscribeToTimeRangeChanges(timeRange);
      if (this.shouldRunQueriesOnActivate()) {
        this.runQueries();
      }
    }
    if (!this._dataLayersSub) {
      this._handleDataLayers();
    }
    return () => this._onDeactivate();
  }
  // This method subscribes to all SceneDataLayers up until the root, and combines the results into data provided from SceneQueryRunner
  _handleDataLayers() {
    const dataLayers = sceneGraph.getDataLayers(this);
    if (dataLayers.length === 0) {
      return;
    }
    this._dataLayersSub = this._dataLayersMerger.getMergedStream(dataLayers).subscribe(this._onLayersReceived.bind(this));
  }
  _onLayersReceived(results) {
    var _a, _b, _c, _d, _e;
    const timeRange = sceneGraph.getTimeRange(this);
    const { dataLayerFilter } = this.state;
    let annotations = [];
    let alertStates = [];
    let alertState;
    for (const result of results) {
      for (let frame of result.data.series) {
        if (((_a = frame.meta) == null ? void 0 : _a.dataTopic) === DataTopic.Annotations) {
          annotations = annotations.concat(frame);
        }
        if (((_b = frame.meta) == null ? void 0 : _b.dataTopic) === DataTopic.AlertStates) {
          alertStates = alertStates.concat(frame);
        }
      }
    }
    if (dataLayerFilter == null ? void 0 : dataLayerFilter.panelId) {
      if (annotations.length > 0) {
        annotations = filterAnnotations(annotations, dataLayerFilter);
      }
      if (alertStates.length > 0) {
        for (const frame of alertStates) {
          const frameView = new DataFrameView(frame);
          for (const row of frameView) {
            if (row.panelId === dataLayerFilter.panelId) {
              alertState = row;
              break;
            }
          }
        }
      }
    }
    if (allFramesEmpty(annotations) && allFramesEmpty(this._layerAnnotations) && isEqual(alertState, (_c = this.state.data) == null ? void 0 : _c.alertState)) {
      return;
    }
    this._layerAnnotations = annotations;
    const baseStateUpdate = this.state.data ? this.state.data : { ...emptyPanelData, timeRange: timeRange.state.value };
    this.setState({
      data: {
        ...baseStateUpdate,
        annotations: [...(_d = this._resultAnnotations) != null ? _d : [], ...annotations],
        alertState: alertState != null ? alertState : (_e = this.state.data) == null ? void 0 : _e.alertState
      }
    });
  }
  /**
   * This tries to start a new query whenever a variable completes or is changed.
   *
   * We care about variable update completions even when the variable has not changed and even when it is not a direct dependency.
   * Example: Variables A and B (B depends on A). A update depends on time range. So when time change query runner will
   * find that variable A is loading which is a dependency on of variable B so will set _isWaitingForVariables to true and
   * not issue any query.
   *
   * When A completes it's loading (with no value change, so B never updates) it will cause a call of this function letting
   * the query runner know that A has completed, and in case _isWaitingForVariables we try to run the query. The query will
   * only run if all variables are in a non loading state so in other scenarios where a query depends on many variables this will
   * be called many times until all dependencies are in a non loading state.   *
   */
  onVariableUpdatesCompleted() {
    if (this.isQueryModeAuto()) {
      this.runQueries();
    }
  }
  /**
   * Check if value changed is an adhoc filter or group by variable that did not exist when we issued the last query
   */
  onAnyVariableChanged(variable) {
    if (this._drilldownDependenciesManager.adHocFiltersVar === variable || this._drilldownDependenciesManager.groupByVar === variable || !this.isQueryModeAuto()) {
      return;
    }
    if (variable instanceof AdHocFiltersVariable && this._isRelevantAutoVariable(variable)) {
      this.runQueries();
    }
    if (variable instanceof GroupByVariable && this._isRelevantAutoVariable(variable)) {
      this.runQueries();
    }
  }
  _isRelevantAutoVariable(variable) {
    var _a, _b;
    const datasource = (_a = this.state.datasource) != null ? _a : findFirstDatasource(this.state.queries);
    return variable.state.applyMode === "auto" && (datasource == null ? void 0 : datasource.uid) === ((_b = variable.state.datasource) == null ? void 0 : _b.uid);
  }
  shouldRunQueriesOnActivate() {
    if (this._variableValueRecorder.hasDependenciesChanged(this)) {
      writeSceneLog(
        "SceneQueryRunner",
        "Variable dependency changed while inactive, shouldRunQueriesOnActivate returns true"
      );
      return true;
    }
    if (!this.state.data) {
      return true;
    }
    if (this._isDataTimeRangeStale(this.state.data)) {
      return true;
    }
    return false;
  }
  _isDataTimeRangeStale(data) {
    const timeRange = sceneGraph.getTimeRange(this);
    const stateTimeRange = timeRange.state.value;
    const dataTimeRange = data.timeRange;
    if (stateTimeRange.from.unix() === dataTimeRange.from.unix() && stateTimeRange.to.unix() === dataTimeRange.to.unix()) {
      return false;
    }
    writeSceneLog("SceneQueryRunner", "Data time range is stale");
    return true;
  }
  _onDeactivate() {
    var _a;
    if (this._querySub) {
      this._querySub.unsubscribe();
      this._querySub = void 0;
    }
    if (this._dataLayersSub) {
      this._dataLayersSub.unsubscribe();
      this._dataLayersSub = void 0;
    }
    (_a = this._timeSub) == null ? void 0 : _a.unsubscribe();
    this._timeSub = void 0;
    this._timeSubRange = void 0;
    if (this.state.data && ![LoadingState.Done, LoadingState.Error].includes(this.state.data.state)) {
      this.setState({ data: void 0 });
    }
    this._drilldownDependenciesManager.cleanup();
  }
  setContainerWidth(width) {
    if (!this._containerWidth && width > 0) {
      this._containerWidth = width;
      if (this.state.maxDataPointsFromWidth && !this.state.maxDataPoints) {
        setTimeout(() => {
          if (this.isActive && !this.state._hasFetchedData) {
            this.runQueries();
          }
        }, 0);
      }
    } else {
      if (width > 0) {
        this._containerWidth = width;
      }
    }
  }
  isDataReadyToDisplay() {
    return Boolean(this.state._hasFetchedData);
  }
  subscribeToTimeRangeChanges(timeRange) {
    if (this._timeSubRange === timeRange) {
      return;
    }
    if (this._timeSub) {
      this._timeSub.unsubscribe();
    }
    this._timeSubRange = timeRange;
    this._timeSub = timeRange.subscribeToState(() => {
      setTimeout(() => this.runWithTimeRange(timeRange), 0);
    });
  }
  runQueries() {
    const timeRange = sceneGraph.getTimeRange(this);
    if (this.isQueryModeAuto()) {
      this.subscribeToTimeRangeChanges(timeRange);
    }
    this.runWithTimeRange(timeRange);
  }
  getMaxDataPoints() {
    var _a;
    if (this.state.maxDataPoints) {
      return this.state.maxDataPoints;
    }
    return this.state.maxDataPointsFromWidth ? (_a = this._containerWidth) != null ? _a : 500 : 500;
  }
  cancelQuery() {
    var _a, _b;
    (_a = this._querySub) == null ? void 0 : _a.unsubscribe();
    if (this._dataLayersSub) {
      this._dataLayersSub.unsubscribe();
      this._dataLayersSub = void 0;
    }
    this.setState({
      data: { ...(_b = this.state.data) != null ? _b : emptyPanelData, state: LoadingState.Done }
    });
  }
  async runWithTimeRange(timeRange) {
    var _a, _b, _c;
    if (!this.state.maxDataPoints && this.state.maxDataPointsFromWidth && !this._containerWidth) {
      return;
    }
    if (this.isQueryModeAuto() && !this._isInView && !this._bypassIsInView) {
      this._queryNotExecutedWhenOutOfView = true;
      return;
    }
    this._queryNotExecutedWhenOutOfView = false;
    if (!this._dataLayersSub) {
      this._handleDataLayers();
    }
    (_a = this._querySub) == null ? void 0 : _a.unsubscribe();
    if (this._variableDependency.hasDependencyInLoadingState()) {
      writeSceneLog("SceneQueryRunner", "Variable dependency is in loading state, skipping query execution");
      this.setState({ data: { ...(_b = this.state.data) != null ? _b : emptyPanelData, state: LoadingState.Loading } });
      return;
    }
    this._variableValueRecorder.recordCurrentDependencyValuesForSceneObject(this);
    const { queries } = this.state;
    if (!(queries == null ? void 0 : queries.length)) {
      this._setNoDataState();
      return;
    }
    try {
      const datasource = (_c = this.state.datasource) != null ? _c : findFirstDatasource(queries);
      const runtimeDatasource = shouldUseMixedRuntimeDatasource(this, datasource, queries) ? MIXED_DATASOURCE_REF : datasource;
      const ds = await getDataSource(runtimeDatasource, this._scopedVars);
      this._drilldownDependenciesManager.findAndSubscribeToDrilldowns(ds.uid, this);
      const runRequest = getRunRequest();
      const { primary, secondaries, processors } = this.prepareRequests(timeRange, ds);
      writeSceneLog("SceneQueryRunner", "Starting runRequest", this.state.key);
      let stream = runRequest(ds, primary);
      if (secondaries.length > 0) {
        const secondaryStreams = secondaries.map((r) => runRequest(ds, r));
        const op = extraQueryProcessingOperator(processors);
        stream = forkJoin([stream, ...secondaryStreams]).pipe(op);
      }
      const panelProfiler = findPanelProfiler(this);
      stream = stream.pipe(
        registerQueryWithController(
          {
            type: "SceneQueryRunner/runQueries",
            request: primary,
            origin: this,
            cancel: () => this.cancelQuery()
          },
          panelProfiler
        )
      );
      this._querySub = stream.subscribe(this.onDataReceived);
    } catch (err) {
      console.error("PanelQueryRunner Error", err);
      this.onDataReceived({
        ...emptyPanelData,
        ...this.state.data,
        state: LoadingState.Error,
        errors: [toDataQueryError(err)]
      });
    }
  }
  clone(withState) {
    var _a;
    const clone = super.clone(withState);
    if (this._resultAnnotations) {
      clone["_resultAnnotations"] = this._resultAnnotations.map((frame) => ({ ...frame }));
    }
    if (this._layerAnnotations) {
      clone["_layerAnnotations"] = this._layerAnnotations.map((frame) => ({ ...frame }));
    }
    clone["_variableValueRecorder"] = this._variableValueRecorder.cloneAndRecordCurrentValuesForSceneObject(this);
    clone["_containerWidth"] = this._containerWidth;
    clone["_results"].next({ origin: this, data: (_a = this.state.data) != null ? _a : emptyPanelData });
    return clone;
  }
  prepareRequests(timeRange, ds) {
    var _a;
    const { minInterval, queries, requestIdPrefix } = this.state;
    let request = {
      app: "scenes",
      requestId: getNextRequestId$1(requestIdPrefix),
      timezone: timeRange.getTimeZone(),
      range: timeRange.state.value,
      interval: "1s",
      intervalMs: 1e3,
      targets: cloneDeep(queries),
      maxDataPoints: this.getMaxDataPoints(),
      scopedVars: this._scopedVars,
      startTime: Date.now(),
      liveStreaming: this.state.liveStreaming,
      rangeRaw: {
        from: timeRange.state.from,
        to: timeRange.state.to
      },
      cacheTimeout: this.state.cacheTimeout,
      queryCachingTTL: this.state.queryCachingTTL,
      scopes: sceneGraph.getScopes(this),
      // This asks the scene root to provide context properties like app, panel and dashboardUID
      ...getEnrichedDataRequest(this)
    };
    const filters = this._drilldownDependenciesManager.getFilters();
    const groupByKeys = this._drilldownDependenciesManager.getGroupByKeys();
    if (filters) {
      request.filters = filters;
    }
    if (groupByKeys) {
      request.groupByKeys = groupByKeys;
    }
    request.targets = request.targets.map((query) => {
      var _a2, _b;
      if (!query.datasource || query.datasource.uid !== ds.uid && !((_a2 = ds.meta) == null ? void 0 : _a2.mixed) && isExpressionReference && !isExpressionReference(query.datasource)) {
        if (!query.datasource && ((_b = ds.meta) == null ? void 0 : _b.mixed) && isTemplatedDatasourceRef(this.state.datasource)) {
          query.datasource = this.state.datasource;
        } else {
          query.datasource = ds.getRef();
        }
      }
      return query;
    });
    const lowerIntervalLimit = minInterval ? interpolate(this, minInterval) : ds.interval;
    const norm = rangeUtil.calculateInterval(timeRange.state.value, request.maxDataPoints, lowerIntervalLimit);
    request.scopedVars = Object.assign({}, request.scopedVars, {
      __interval: { text: norm.interval, value: norm.interval },
      __interval_ms: { text: norm.intervalMs.toString(), value: norm.intervalMs }
    });
    request.interval = norm.interval;
    request.intervalMs = norm.intervalMs;
    const primaryTimeRange = timeRange.state.value;
    let secondaryRequests = [];
    let secondaryProcessors = /* @__PURE__ */ new Map();
    for (const provider of (_a = this.getClosestExtraQueryProviders()) != null ? _a : []) {
      for (const { req, processor } of provider.getExtraQueries(request)) {
        const requestId = getNextRequestId$1(requestIdPrefix);
        secondaryRequests.push({ ...req, requestId });
        secondaryProcessors.set(requestId, processor != null ? processor : passthroughProcessor);
      }
    }
    request.range = primaryTimeRange;
    return { primary: request, secondaries: secondaryRequests, processors: secondaryProcessors };
  }
  _combineDataLayers(data) {
    if (this._layerAnnotations && this._layerAnnotations.length > 0) {
      data.annotations = (data.annotations || []).concat(this._layerAnnotations);
    }
    if (this.state.data && this.state.data.alertState) {
      data.alertState = this.state.data.alertState;
    }
    return data;
  }
  _setNoDataState() {
    if (this.state.data !== emptyPanelData) {
      this.setState({ data: emptyPanelData });
    }
  }
  /**
   * Walk up the scene graph and find any ExtraQueryProviders.
   *
   * This will return an array of the closest provider of each type.
   */
  getClosestExtraQueryProviders() {
    const found = /* @__PURE__ */ new Map();
    if (!this.parent) {
      return [];
    }
    getClosest(this.parent, (s) => {
      if (isExtraQueryProvider(s) && !found.has(s.constructor)) {
        found.set(s.constructor, s);
      }
      s.forEachChild((child) => {
        if (isExtraQueryProvider(child) && !found.has(child.constructor)) {
          found.set(child.constructor, child);
        }
      });
      return null;
    });
    return Array.from(found.values());
  }
  isQueryModeAuto() {
    var _a;
    return ((_a = this.state.runQueriesMode) != null ? _a : "auto") === "auto";
  }
  isInViewChanged(isInView) {
    writeSceneLog("SceneQueryRunner", `isInViewChanged: ${isInView}`, this.state.key);
    this._isInView = isInView;
    if (isInView && this._queryNotExecutedWhenOutOfView) {
      this.runQueries();
    }
  }
  bypassIsInViewChanged(bypassIsInView) {
    writeSceneLog("SceneQueryRunner", `bypassIsInViewChanged: ${bypassIsInView}`, this.state.key);
    this._bypassIsInView = bypassIsInView;
    if (bypassIsInView && this._queryNotExecutedWhenOutOfView) {
      this.runQueries();
    }
  }
}
function findFirstDatasource(targets) {
  var _a, _b;
  return (_b = (_a = targets.find((t) => t.datasource !== null)) == null ? void 0 : _a.datasource) != null ? _b : void 0;
}
function allFramesEmpty(frames) {
  if (!frames) {
    return true;
  }
  for (let i = 0; i < frames.length; i++) {
    if (frames[i].length > 0) {
      return false;
    }
  }
  return true;
}

export { SceneQueryRunner, findFirstDatasource, getNextRequestId$1 as getNextRequestId };
//# sourceMappingURL=SceneQueryRunner.js.map
