import { omit } from 'lodash';

import { AnnotationQuery } from '@grafana/data';
import { config } from '@grafana/runtime';
import {
  behaviors,
  dataLayers,
  QueryVariable,
  SceneDataQuery,
  SceneDataTransformer,
  SceneQueryRunner,
  SceneVariables,
  SceneVariableSet,
  VizPanel,
} from '@grafana/scenes';
import { DataSourceRef } from '@grafana/schema';
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';

import {
  Spec as DashboardV2Spec,
  defaultSpec as defaultDashboardV2Spec,
  defaultFieldConfigSource,
  PanelKind,
  PanelQueryKind,
  TransformationKind,
  FieldConfigSource,
  DataTransformerConfig,
  PanelQuerySpec,
  DataQueryKind,
  QueryOptionsSpec,
  QueryVariableKind,
  TextVariableKind,
  IntervalVariableKind,
  DatasourceVariableKind,
  CustomVariableKind,
  ConstantVariableKind,
  GroupByVariableKind,
  AdhocVariableKind,
  AnnotationQueryKind,
  DataLink,
  LibraryPanelKind,
  Element,
  DashboardCursorSync,
  FieldConfig,
  FieldColor,
} from '../../../../../packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getLibraryPanelBehavior, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils';

import { DSReferencesMapping } from './DashboardSceneSerializer';
import { sceneVariablesSetToSchemaV2Variables } from './sceneVariablesSetToVariables';
import { colorIdEnumToColorIdV2, transformCursorSynctoEnum } from './transformToV2TypesUtils';

// FIXME: This is temporary to avoid creating partial types for all the new schema, it has some performance implications, but it's fine for now
type DeepPartial<T> = T extends object
  ? {
      [P in keyof T]?: DeepPartial<T[P]>;
    }
  : T;

export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnapshot = false): DashboardV2Spec {
  const sceneDash = scene.state;
  const timeRange = sceneDash.$timeRange!.state;

  const controlsState = sceneDash.controls?.state;
  const refreshPicker = controlsState?.refreshPicker;

  const dsReferencesMapping: DSReferencesMapping = scene.serializer.getDSReferencesMapping();

  const dashboardSchemaV2: DeepPartial<DashboardV2Spec> = {
    //dashboard settings
    title: sceneDash.title,
    description: sceneDash.description,
    cursorSync: getCursorSync(sceneDash),
    liveNow: getLiveNow(sceneDash),
    preload: sceneDash.preload ?? defaultDashboardV2Spec().preload,
    editable: sceneDash.editable ?? defaultDashboardV2Spec().editable,
    links: sceneDash.links ?? defaultDashboardV2Spec().links,
    tags: sceneDash.tags ?? defaultDashboardV2Spec().tags,
    // EOF dashboard settings

    // time settings
    timeSettings: {
      timezone: timeRange.timeZone,
      from: timeRange.from,
      to: timeRange.to,
      autoRefresh: refreshPicker?.state.refresh || '',
      autoRefreshIntervals: refreshPicker?.state.intervals,
      hideTimepicker: controlsState?.hideTimeControls ?? false,
      weekStart: timeRange.weekStart,
      fiscalYearStartMonth: timeRange.fiscalYearStartMonth,
      nowDelay: timeRange.UNSAFE_nowDelay,
      quickRanges: controlsState?.timePicker.state.quickRanges,
    },
    // EOF time settings

    // variables
    variables: getVariables(sceneDash, dsReferencesMapping),
    // EOF variables

    // elements
    elements: getElements(scene, dsReferencesMapping),
    // EOF elements

    // annotations
    annotations: getAnnotations(sceneDash, dsReferencesMapping),
    // EOF annotations

    // layout
    layout: sceneDash.body.serialize(),
    // EOF layout
  };

  try {
    // validateDashboardSchemaV2 will throw an error if the dashboard is not valid
    if (validateDashboardSchemaV2(dashboardSchemaV2)) {
      return sortedDeepCloneWithoutNulls(dashboardSchemaV2, true);
    }
    // should never reach this point, validation should throw an error
    throw new Error('Error we could transform the dashboard to schema v2: ' + dashboardSchemaV2);
  } catch (reason) {
    console.error('Error transforming dashboard to schema v2: ' + reason, dashboardSchemaV2);
    throw new Error('Error transforming dashboard to schema v2: ' + reason);
  }
}

function getCursorSync(state: DashboardSceneState) {
  const cursorSync = state.$behaviors?.find((b): b is behaviors.CursorSync => b instanceof behaviors.CursorSync)?.state
    .sync;

  return transformCursorSynctoEnum(cursorSync);
}

function getLiveNow(state: DashboardSceneState) {
  const liveNow =
    state.$behaviors?.find((b): b is behaviors.LiveNowTimer => b instanceof behaviors.LiveNowTimer)?.isEnabled ||
    undefined;
  // hack for validator
  if (liveNow === undefined) {
    return Boolean(defaultDashboardV2Spec().liveNow);
  }
  return Boolean(liveNow);
}

function getElements(scene: DashboardScene, dsReferencesMapping: DSReferencesMapping) {
  const panels = scene.state.body.getVizPanels() ?? [];
  const panelsArray = panels.map((vizPanel) => {
    return vizPanelToSchemaV2(vizPanel, dsReferencesMapping);
  });
  return createElements(panelsArray, scene);
}

export function vizPanelToSchemaV2(
  vizPanel: VizPanel,
  dsReferencesMapping?: DSReferencesMapping
): PanelKind | LibraryPanelKind {
  if (isLibraryPanel(vizPanel)) {
    const behavior = getLibraryPanelBehavior(vizPanel)!;
    const elementSpec: LibraryPanelKind = {
      kind: 'LibraryPanel',
      spec: {
        id: getPanelIdForVizPanel(vizPanel),
        title: vizPanel.state.title,
        libraryPanel: {
          uid: behavior.state.uid,
          name: behavior.state.name,
        },
      },
    };
    return elementSpec;
  }

  // Handle type conversion for color mode
  const rawColor = vizPanel.state.fieldConfig.defaults.color;
  let color: FieldColor | undefined;

  if (rawColor) {
    const convertedMode = colorIdEnumToColorIdV2(rawColor.mode);

    if (convertedMode) {
      color = {
        ...rawColor,
        mode: convertedMode,
      };
    }
  }

  // Remove null from the defaults because schema V2 doesn't support null for these fields
  const decimals = vizPanel.state.fieldConfig.defaults.decimals ?? undefined;
  const min = vizPanel.state.fieldConfig.defaults.min ?? undefined;
  const max = vizPanel.state.fieldConfig.defaults.max ?? undefined;

  const defaults: FieldConfig = Object.fromEntries(
    Object.entries({
      ...vizPanel.state.fieldConfig.defaults,
      decimals,
      min,
      max,
      color,
    }).filter(([_, value]) => {
      if (Array.isArray(value)) {
        return value.length > 0;
      }
      return value !== undefined;
    })
  );

  const vizFieldConfig: FieldConfigSource = {
    ...vizPanel.state.fieldConfig,
    defaults,
  };

  const elementSpec: PanelKind = {
    kind: 'Panel',
    spec: {
      id: getPanelIdForVizPanel(vizPanel),
      title: vizPanel.state.title,
      description: vizPanel.state.description ?? '',
      links: getPanelLinks(vizPanel),
      transparent: vizPanel.state.displayMode === 'transparent' ? true : undefined,
      data: {
        kind: 'QueryGroup',
        spec: {
          queries: getVizPanelQueries(vizPanel, dsReferencesMapping),
          transformations: getVizPanelTransformations(vizPanel),
          queryOptions: getVizPanelQueryOptions(vizPanel),
        },
      },
      vizConfig: {
        kind: vizPanel.state.pluginId,
        spec: {
          pluginVersion: vizPanel.state.pluginVersion ?? '',
          options: vizPanel.state.options,
          fieldConfig: vizFieldConfig ?? defaultFieldConfigSource(),
        },
      },
    },
  };
  return elementSpec;
}

function getPanelLinks(panel: VizPanel): DataLink[] {
  const vizLinks = dashboardSceneGraph.getPanelLinks(panel);
  if (vizLinks) {
    return vizLinks.state.rawLinks ?? [];
  }
  return [];
}

function getVizPanelQueries(vizPanel: VizPanel, dsReferencesMapping?: DSReferencesMapping): PanelQueryKind[] {
  const queries: PanelQueryKind[] = [];
  const queryRunner = getQueryRunnerFor(vizPanel);
  const vizPanelQueries = queryRunner?.state.queries;

  if (vizPanelQueries) {
    vizPanelQueries.forEach((query) => {
      const queryDatasource = getElementDatasource(vizPanel, query, 'panel', queryRunner, dsReferencesMapping);
      const dataQuery: DataQueryKind = {
        kind: getDataQueryKind(query, queryRunner),
        spec: omit(query, 'datasource', 'refId', 'hide'),
      };
      const querySpec: PanelQuerySpec = {
        datasource: queryDatasource,
        query: dataQuery,
        refId: query.refId,
        hidden: Boolean(query.hide),
      };
      queries.push({
        kind: 'PanelQuery',
        spec: querySpec,
      });
    });
  }
  return queries;
}

export function getDataQueryKind(query: SceneDataQuery | string, queryRunner?: SceneQueryRunner): string {
  // Query is a string - get default data source type
  if (typeof query === 'string') {
    const defaultDS = getDefaultDataSourceRef();
    return defaultDS?.type || '';
  }

  // Query has explicit datasource with type
  if (query.datasource?.type) {
    return query.datasource.type;
  }

  // Get type from query runner's datasource
  if (queryRunner?.state.datasource?.type) {
    return queryRunner.state.datasource.type;
  }

  // Fall back to default datasource
  const defaultDS = getDefaultDataSourceRef();
  return defaultDS?.type || '';
}

export function getDataQuerySpec(query: SceneDataQuery): DataQueryKind['spec'] {
  return query;
}

function getVizPanelTransformations(vizPanel: VizPanel): TransformationKind[] {
  let transformations: TransformationKind[] = [];
  const dataProvider = vizPanel.state.$data;
  if (dataProvider instanceof SceneDataTransformer) {
    const transformationList = dataProvider.state.transformations;

    if (transformationList.length === 0) {
      return [];
    }

    for (const transformationItem of transformationList) {
      const transformation = transformationItem;

      if ('id' in transformation) {
        // Transformation is a DataTransformerConfig
        const transformationSpec: DataTransformerConfig = {
          id: transformation.id,
          disabled: transformation.disabled,
          filter: transformation.filter,
          ...(transformation.topic && { topic: transformation.topic }),
          options: transformation.options,
        };

        transformations.push({
          kind: transformation.id,
          spec: transformationSpec,
        });
      } else {
        throw new Error('Unsupported transformation type');
      }
    }
  }
  return transformations;
}

function getVizPanelQueryOptions(vizPanel: VizPanel): QueryOptionsSpec {
  let queryOptions: QueryOptionsSpec = {};
  const queryRunner = getQueryRunnerFor(vizPanel);

  if (queryRunner) {
    queryOptions.maxDataPoints = queryRunner.state.maxDataPoints;

    if (queryRunner.state.cacheTimeout) {
      queryOptions.cacheTimeout = queryRunner.state.cacheTimeout;
    }

    if (queryRunner.state.queryCachingTTL) {
      queryOptions.queryCachingTTL = queryRunner.state.queryCachingTTL;
    }
    if (queryRunner.state.minInterval) {
      queryOptions.interval = queryRunner.state.minInterval;
    }
  }

  const panelTime = vizPanel.state.$timeRange;

  if (panelTime instanceof PanelTimeRange) {
    queryOptions.timeFrom = panelTime.state.timeFrom;
    queryOptions.timeShift = panelTime.state.timeShift;
    queryOptions.hideTimeOverride = panelTime.state.hideTimeOverride;
  }
  return queryOptions;
}

export function createElements(panels: Element[], scene: DashboardScene): Record<string, Element> {
  return panels.reduce<Record<string, Element>>((elements, panel) => {
    let elementKey = scene.serializer.getElementIdForPanel(panel.spec.id);
    elements[elementKey!] = panel;
    return elements;
  }, {});
}

function getVariables(oldDash: DashboardSceneState, dsReferencesMapping?: DSReferencesMapping) {
  const variablesSet = oldDash.$variables;

  // variables is an array of all variables kind (union)
  let variables: Array<
    | QueryVariableKind
    | TextVariableKind
    | IntervalVariableKind
    | DatasourceVariableKind
    | CustomVariableKind
    | ConstantVariableKind
    | GroupByVariableKind
    | AdhocVariableKind
  > = [];

  if (variablesSet instanceof SceneVariableSet) {
    variables = sceneVariablesSetToSchemaV2Variables(variablesSet, false, dsReferencesMapping);
  }

  return variables;
}

function getAnnotations(state: DashboardSceneState, dsReferencesMapping?: DSReferencesMapping): AnnotationQueryKind[] {
  const data = state.$data;
  if (!(data instanceof DashboardDataLayerSet)) {
    return [];
  }
  const annotations: AnnotationQueryKind[] = [];
  for (const layer of data.state.annotationLayers) {
    if (!(layer instanceof dataLayers.AnnotationsDataLayer)) {
      continue;
    }
    const result: AnnotationQueryKind = {
      kind: 'AnnotationQuery',
      spec: {
        builtIn: Boolean(layer.state.query.builtIn),
        name: layer.state.query.name,
        datasource: getElementDatasource(layer, layer.state.query, 'annotation', undefined, dsReferencesMapping),
        enable: Boolean(layer.state.isEnabled),
        hide: Boolean(layer.state.isHidden),
        iconColor: layer.state.query.iconColor,
      },
    };

    // Transform v1 dashboard (using target) to v2 structure
    // adds extra condition to prioritize query over target
    // if query is defined, use it
    if (layer.state.query.target && !layer.state.query.query) {
      // Handle built-in annotations
      if (layer.state.query.builtIn) {
        result.spec.query = {
          kind: 'grafana', // built-in annotations are always of type grafana
          spec: {
            ...layer.state.query.target,
          },
        };
      } else {
        result.spec.query = {
          kind: getAnnotationQueryKind(layer.state.query),
          spec: {
            ...layer.state.query.target,
          },
        };
      }
    }
    // For annotations without query.query defined (e.g., grafana annotations without tags)
    else if (layer.state.query.query?.kind) {
      result.spec.query = {
        kind: layer.state.query.query.kind,
        spec: {
          ...layer.state.query.query.spec,
        },
      };
    }
    // Collect datasource-specific properties not in standard annotation spec
    let otherProps = omit(
      layer.state.query,
      'type',
      'target',
      'builtIn',
      'name',
      'datasource',
      'iconColor',
      'enable',
      'hide',
      'filter',
      'query'
    );

    // Store extra properties in the legacyOptions field instead of directly in the spec
    if (Object.keys(otherProps).length > 0) {
      // Extract options property and get the rest of the properties
      const { legacyOptions, ...restProps } = otherProps;
      // Merge options with the rest of the properties
      result.spec.legacyOptions = { ...legacyOptions, ...restProps };
    }

    // If filter is an empty array, don't save it
    if (layer.state.query.filter?.ids?.length) {
      result.spec.filter = layer.state.query.filter;
    }

    annotations.push(result);
  }
  return annotations;
}

export function getAnnotationQueryKind(annotationQuery: AnnotationQuery): string {
  if (annotationQuery.datasource?.type) {
    return annotationQuery.datasource.type;
  } else {
    const ds = getDefaultDataSourceRef();
    if (ds) {
      return ds.type!; // in the datasource list from bootData "id" is the type
    }
    // if we can't find the default datasource, return grafana as default
    return 'grafana';
  }
}

export function getDefaultDataSourceRef(): DataSourceRef {
  // we need to return the default datasource configured in the BootConfig
  const defaultDatasource = config.bootData.settings.defaultDatasource;

  // get default datasource type
  const dsList = config.bootData.settings.datasources;
  const ds = dsList[defaultDatasource];

  return { type: ds.meta.id, uid: ds.name }; // in the datasource list from bootData "id" is the type
}

// Function to know if the dashboard transformed is a valid DashboardV2Spec
export function validateDashboardSchemaV2(dash: unknown): dash is DashboardV2Spec {
  if (typeof dash !== 'object' || dash === null || Array.isArray(dash)) {
    throw new Error('Dashboard is not an object or is null');
  }

  // Required properties
  if (!('title' in dash) || typeof dash.title !== 'string') {
    throw new Error('Title is not a string');
  }
  if (!('timeSettings' in dash) || typeof dash.timeSettings !== 'object' || dash.timeSettings === null) {
    throw new Error('TimeSettings is not an object or is null');
  }
  if (!('variables' in dash) || !Array.isArray(dash.variables)) {
    throw new Error('Variables is not an array');
  }
  if (!('elements' in dash) || typeof dash.elements !== 'object' || dash.elements === null) {
    throw new Error('Elements is not an object or is null');
  }
  if (!('annotations' in dash) || !Array.isArray(dash.annotations)) {
    throw new Error('Annotations is not an array');
  }
  if (!('layout' in dash) || typeof dash.layout !== 'object' || dash.layout === null) {
    throw new Error('Layout is not an object or is null');
  }

  // Optional properties - only validate if present
  if ('description' in dash && dash.description !== undefined && typeof dash.description !== 'string') {
    throw new Error('Description is not a string');
  }
  if ('cursorSync' in dash && dash.cursorSync !== undefined) {
    const validCursorSyncValues = ((): string[] => {
      const typeValues: DashboardCursorSync[] = ['Off', 'Crosshair', 'Tooltip'];
      return typeValues;
    })();

    if (typeof dash.cursorSync !== 'string' || !validCursorSyncValues.includes(dash.cursorSync)) {
      throw new Error('CursorSync is not a valid value');
    }
  }
  if ('liveNow' in dash && dash.liveNow !== undefined && typeof dash.liveNow !== 'boolean') {
    throw new Error('LiveNow is not a boolean');
  }
  if ('preload' in dash && dash.preload !== undefined && typeof dash.preload !== 'boolean') {
    throw new Error('Preload is not a boolean');
  }
  if ('editable' in dash && dash.editable !== undefined && typeof dash.editable !== 'boolean') {
    throw new Error('Editable is not a boolean');
  }
  if ('links' in dash && dash.links !== undefined && !Array.isArray(dash.links)) {
    throw new Error('Links is not an array');
  }
  if ('tags' in dash && dash.tags !== undefined && !Array.isArray(dash.tags)) {
    throw new Error('Tags is not an array');
  }
  if ('id' in dash && dash.id !== undefined && typeof dash.id !== 'number') {
    throw new Error('ID is not a number');
  }

  // Time settings validation
  const timeSettings = dash.timeSettings;

  // Required time settings
  if (!('from' in timeSettings) || typeof timeSettings.from !== 'string') {
    throw new Error('From is not a string');
  }
  if (!('to' in timeSettings) || typeof timeSettings.to !== 'string') {
    throw new Error('To is not a string');
  }
  if (!('autoRefresh' in timeSettings) || typeof timeSettings.autoRefresh !== 'string') {
    throw new Error('AutoRefresh is not a string');
  }
  if (!('hideTimepicker' in timeSettings) || typeof timeSettings.hideTimepicker !== 'boolean') {
    throw new Error('HideTimepicker is not a boolean');
  }

  // Optional time settings with defaults
  if (
    'autoRefreshIntervals' in timeSettings &&
    timeSettings.autoRefreshIntervals !== undefined &&
    !Array.isArray(timeSettings.autoRefreshIntervals)
  ) {
    throw new Error('AutoRefreshIntervals is not an array');
  }
  if ('timezone' in timeSettings && timeSettings.timezone !== undefined && typeof timeSettings.timezone !== 'string') {
    throw new Error('Timezone is not a string');
  }
  if (
    'quickRanges' in timeSettings &&
    timeSettings.quickRanges !== undefined &&
    !Array.isArray(timeSettings.quickRanges)
  ) {
    throw new Error('QuickRanges is not an array');
  }
  if ('weekStart' in timeSettings && timeSettings.weekStart !== undefined) {
    if (
      typeof timeSettings.weekStart !== 'string' ||
      !['saturday', 'sunday', 'monday'].includes(timeSettings.weekStart)
    ) {
      throw new Error('WeekStart should be one of "saturday", "sunday" or "monday"');
    }
  }
  if ('nowDelay' in timeSettings && timeSettings.nowDelay !== undefined && typeof timeSettings.nowDelay !== 'string') {
    throw new Error('NowDelay is not a string');
  }
  if (
    'fiscalYearStartMonth' in timeSettings &&
    timeSettings.fiscalYearStartMonth !== undefined &&
    typeof timeSettings.fiscalYearStartMonth !== 'number'
  ) {
    throw new Error('FiscalYearStartMonth is not a number');
  }

  // Layout validation
  if (!('kind' in dash.layout)) {
    throw new Error('Layout kind is required');
  }

  if (dash.layout.kind === 'GridLayout') {
    validateGridLayout(dash.layout);
  } else if (dash.layout.kind === 'RowsLayout') {
    validateRowsLayout(dash.layout);
  }

  return true;
}

function validateGridLayout(layout: unknown) {
  if (typeof layout !== 'object' || layout === null) {
    throw new Error('Layout is not an object or is null');
  }
  if (!('kind' in layout) || layout.kind !== 'GridLayout') {
    throw new Error('Layout kind is not GridLayout');
  }
  if (!('spec' in layout) || typeof layout.spec !== 'object' || layout.spec === null) {
    throw new Error('Layout spec is not an object or is null');
  }
  if (!('items' in layout.spec) || !Array.isArray(layout.spec.items)) {
    throw new Error('Layout spec items is not an array');
  }
}

function validateRowsLayout(layout: unknown) {
  if (typeof layout !== 'object' || layout === null) {
    throw new Error('Layout is not an object or is null');
  }
  if (!('kind' in layout) || layout.kind !== 'RowsLayout') {
    throw new Error('Layout kind is not RowsLayout');
  }
  if (!('spec' in layout) || typeof layout.spec !== 'object' || layout.spec === null) {
    throw new Error('Layout spec is not an object or is null');
  }
  if (!('rows' in layout.spec) || !Array.isArray(layout.spec.rows)) {
    throw new Error('Layout spec items is not an array');
  }
}

export function getAutoAssignedDSRef(
  element: VizPanel | SceneVariables | dataLayers.AnnotationsDataLayer,
  type: 'panels' | 'variables' | 'annotations',
  elementMapReferences?: DSReferencesMapping
): Set<string> {
  if (!elementMapReferences) {
    return new Set();
  }
  if (type === 'panels' && isVizPanel(element)) {
    const elementKey = dashboardSceneGraph.getElementIdentifierForVizPanel(element);
    return elementMapReferences.panels.get(elementKey) || new Set();
  }

  if (type === 'variables') {
    return elementMapReferences.variables;
  }

  if (type === 'annotations') {
    return elementMapReferences.annotations;
  }

  // if type is not panels, annotations, or variables, throw error
  throw new Error(`Invalid type ${type} for getAutoAssignedDSRef`);
}

/**
 * Determines if a data source reference should be persisted for a query or variable
 */
export function getPersistedDSFor<T extends SceneDataQuery | QueryVariable | AnnotationQuery>(
  element: T,
  autoAssignedDsRef: Set<string>,
  type: 'query' | 'variable' | 'annotation',
  context?: SceneQueryRunner
): DataSourceRef | undefined {
  // Get the element identifier - refId for queries, name for variables
  const elementId = getElementIdentifier(element, type);

  // If the element is in the auto-assigned set, it didn't have a datasource specified
  if (autoAssignedDsRef?.has(elementId)) {
    return undefined;
  }

  // Return appropriate datasource reference based on element type
  if (type === 'query') {
    if ('datasource' in element && element.datasource) {
      // If element has its own datasource, use that
      return element.datasource;
    }

    // For queries missing a datasource but not in auto-assigned set, use datasource from context (queryRunner)
    return context?.state?.datasource;
  }

  if (type === 'variable' && 'state' in element && 'datasource' in element.state) {
    return element.state.datasource || {};
  }

  if (type === 'annotation' && 'datasource' in element) {
    return element.datasource || {};
  }

  return undefined;
}

/**
 * Helper function to extract which identifier to use from a query or variable element
 * @returns refId for queries, name for variables
 * TODO: we will add annotations in the future
 */
function getElementIdentifier<T extends SceneDataQuery | QueryVariable | AnnotationQuery>(
  element: T,
  type: 'query' | 'variable' | 'annotation'
): string {
  // when is type query look for refId
  if (type === 'query') {
    return 'refId' in element ? element.refId : '';
  }

  if (type === 'variable') {
    // when is type variable look for the name of the variable
    return 'state' in element && 'name' in element.state ? element.state.name : '';
  }

  // when is type annotation look for annotation name
  if (type === 'annotation') {
    return 'name' in element ? element.name : '';
  }

  throw new Error(`Invalid type ${type} for getElementIdentifier`);
}

function isVizPanel(element: VizPanel | SceneVariables | dataLayers.AnnotationsDataLayer): element is VizPanel {
  // FIXME: is there another way to do this?
  return 'pluginId' in element.state;
}

function isSceneVariables(
  element: VizPanel | SceneVariables | dataLayers.AnnotationsDataLayer
): element is SceneVariables {
  // Check for properties unique to SceneVariables but not in VizPanel
  return !('pluginId' in element.state) && ('variables' in element.state || 'getValue' in element);
}

function isSceneDataQuery(query: SceneDataQuery | QueryVariable | AnnotationQuery): query is SceneDataQuery {
  return 'refId' in query && !('state' in query);
}

function isAnnotationQuery(query: SceneDataQuery | QueryVariable | AnnotationQuery): query is AnnotationQuery {
  return 'datasource' in query && 'name' in query;
}

function isQueryVariable(query: SceneDataQuery | QueryVariable | AnnotationQuery): query is QueryVariable {
  return 'state' in query && 'name' in query.state;
}

/**
 * Get the persisted datasource for a query or variable
 * When a query or variable is created it could not have a datasource set
 * we want to respect that and not overwrite it with the auto assigned datasources
 * resolved in runtime
 *
 */
export function getElementDatasource(
  element: VizPanel | SceneVariables | dataLayers.AnnotationsDataLayer,
  queryElement: SceneDataQuery | QueryVariable | AnnotationQuery,
  type: 'panel' | 'variable' | 'annotation',
  queryRunner?: SceneQueryRunner,
  dsReferencesMapping?: DSReferencesMapping
): DataSourceRef | undefined {
  let result: DataSourceRef | undefined;
  if (type === 'panel') {
    if (!queryRunner || !isVizPanel(element) || !isSceneDataQuery(queryElement)) {
      return undefined;
    }
    // Get datasource for panel query
    const autoAssignedRefs = getAutoAssignedDSRef(element, 'panels', dsReferencesMapping);
    result = getPersistedDSFor(queryElement, autoAssignedRefs, 'query', queryRunner);
  }

  if (type === 'variable') {
    if (!isSceneVariables(element) || !isQueryVariable(queryElement)) {
      return undefined;
    }
    // Get datasource for variable
    const autoAssignedRefs = getAutoAssignedDSRef(element, 'variables', dsReferencesMapping);

    result = getPersistedDSFor(queryElement, autoAssignedRefs, 'variable');
  }

  if (type === 'annotation') {
    if (!isAnnotationQuery(queryElement)) {
      return undefined;
    }
    // Get datasource for annotation
    const autoAssignedRefs = getAutoAssignedDSRef(element, 'annotations', dsReferencesMapping);
    result = getPersistedDSFor(queryElement, autoAssignedRefs, 'annotation');
  }
  // Important: Only return the datasource if it's not in auto-assigned refs
  // and if the result would not be an empty object
  return Object.keys(result || {}).length > 0 ? result : undefined;
}
