import { CoreApp, DataSourceApi, DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config, getDataSourceSrv, locationService } from '@grafana/runtime';
import {
  SceneObjectBase,
  SceneComponentProps,
  sceneGraph,
  SceneQueryRunner,
  SceneObjectRef,
  VizPanel,
  SceneObjectState,
  SceneDataQuery,
} from '@grafana/scenes';
import { DataQuery } from '@grafana/schema';
import { Button, Stack, Tab } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { addQuery } from 'app/core/utils/query';
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard';
import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils';
import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
import { GroupActionComponents } from 'app/features/query/components/QueryActionComponent';
import { QueryEditorRows } from 'app/features/query/components/QueryEditorRows';
import { QueryGroupTopSection } from 'app/features/query/components/QueryGroup';
import { updateQueries } from 'app/features/query/state/updateQueries';
import { isSharedDashboardQuery } from 'app/plugins/datasource/dashboard/runSharedRequest';
import { QueryGroupOptions } from 'app/types';

import { MIXED_DATASOURCE_NAME } from '../../../../plugins/datasource/mixed/MixedDataSource';
import { useQueryLibraryContext } from '../../../explore/QueryLibrary/QueryLibraryContext';
import { ExpressionDatasourceUID } from '../../../expressions/types';
import { getDatasourceSrv } from '../../../plugins/datasource_srv';
import { PanelTimeRange } from '../../scene/PanelTimeRange';
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../../utils/utils';
import { getUpdatedHoverHeader } from '../getPanelFrameOptions';

import { PanelDataPaneTab, TabId, PanelDataTabHeaderProps } from './types';

interface PanelDataQueriesTabState extends SceneObjectState {
  datasource?: DataSourceApi;
  dsSettings?: DataSourceInstanceSettings;
  panelRef: SceneObjectRef<VizPanel>;
}
export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabState> implements PanelDataPaneTab {
  static Component = PanelDataQueriesTabRendered;
  tabId = TabId.Queries;

  public constructor(state: PanelDataQueriesTabState) {
    super(state);
    this.addActivationHandler(() => this.onActivate());
  }

  public getTabLabel() {
    return 'Queries';
  }

  public getItemsCount() {
    return this.getQueries().length;
  }

  public renderTab(props: PanelDataTabHeaderProps) {
    return <QueriesTab key={this.getTabLabel()} model={this} {...props} />;
  }

  private onActivate() {
    this.loadDataSource();
  }

  private async loadDataSource() {
    const panel = this.state.panelRef.resolve();
    const dataObj = panel.state.$data;

    if (!dataObj) {
      return;
    }

    let datasourceToLoad = this.queryRunner.state.datasource;

    try {
      let datasource: DataSourceApi | undefined;
      let dsSettings: DataSourceInstanceSettings | undefined;

      if (!datasourceToLoad) {
        const dashboardScene = getDashboardSceneFor(this);
        const dashboardUid = dashboardScene.state.uid ?? '';
        const lastUsedDatasource = getLastUsedDatasourceFromStorage(dashboardUid!);

        // do we have a last used datasource for this dashboard
        if (lastUsedDatasource?.datasourceUid !== null) {
          // get datasource from dashbopard uid
          dsSettings = getDataSourceSrv().getInstanceSettings({ uid: lastUsedDatasource?.datasourceUid });
          if (dsSettings) {
            datasource = await getDataSourceSrv().get({
              uid: lastUsedDatasource?.datasourceUid,
              type: dsSettings.type,
            });

            this.queryRunner.setState({
              datasource: {
                ...getDataSourceRef(dsSettings),
                uid: lastUsedDatasource?.datasourceUid,
              },
            });
          }
        }
      } else {
        datasource = await getDataSourceSrv().get(datasourceToLoad);
        dsSettings = getDataSourceSrv().getInstanceSettings(datasourceToLoad);
      }

      if (datasource && dsSettings) {
        this.setState({ datasource, dsSettings });
        storeLastUsedDataSourceInLocalStorage(getDataSourceRef(dsSettings) || { default: true });
      }
    } catch (err) {
      //set default datasource if we fail to load the datasource
      const datasource = await getDataSourceSrv().get(config.defaultDatasource);
      const dsSettings = getDataSourceSrv().getInstanceSettings(config.defaultDatasource);

      if (datasource && dsSettings) {
        this.setState({
          datasource,
          dsSettings,
        });

        this.queryRunner.setState({
          datasource: getDataSourceRef(dsSettings),
        });
      }

      console.error(err);
    }
  }

  public buildQueryOptions(): QueryGroupOptions {
    const panel = this.state.panelRef.resolve();
    const queryRunner = getQueryRunnerFor(panel)!;
    const timeRangeObj = sceneGraph.getTimeRange(panel);

    let timeRangeOpts: QueryGroupOptions['timeRange'] = {
      from: undefined,
      shift: undefined,
      hide: undefined,
    };

    if (timeRangeObj instanceof PanelTimeRange) {
      timeRangeOpts = {
        from: timeRangeObj.state.timeFrom,
        shift: timeRangeObj.state.timeShift,
        hide: timeRangeObj.state.hideTimeOverride,
      };
    }

    let queries: QueryGroupOptions['queries'] = queryRunner.state.queries;
    const dsSettings = this.state.dsSettings;

    return {
      cacheTimeout: dsSettings?.meta.queryOptions?.cacheTimeout ? queryRunner.state.cacheTimeout : undefined,
      queryCachingTTL: dsSettings?.cachingConfig?.enabled ? queryRunner.state.queryCachingTTL : undefined,
      dataSource: {
        default: dsSettings?.isDefault,
        ...(dsSettings ? getDataSourceRef(dsSettings) : { type: undefined, uid: undefined }),
      },
      queries,
      maxDataPoints: queryRunner.state.maxDataPoints,
      minInterval: queryRunner.state.minInterval,
      timeRange: timeRangeOpts,
    };
  }

  public onOpenInspector = () => {
    const panel = this.state.panelRef.resolve();
    const panelId = getPanelIdForVizPanel(panel);

    locationService.partial({ inspect: panelId, inspectTab: 'query' });
  };

  public onChangeDataSource = async (newSettings: DataSourceInstanceSettings, defaultQueries?: SceneDataQuery[]) => {
    const { dsSettings } = this.state;
    const queryRunner = this.queryRunner;

    const currentDS = dsSettings ? await getDataSourceSrv().get({ uid: dsSettings.uid }) : undefined;
    const nextDS = await getDataSourceSrv().get({ uid: newSettings.uid });

    const currentQueries = queryRunner.state.queries;

    // We need to pass in newSettings.uid as well here as that can be a variable expression and we want to store that in the query model not the current ds variable value
    const queries = defaultQueries || (await updateQueries(nextDS, newSettings.uid, currentQueries, currentDS));

    queryRunner.setState({ datasource: getDataSourceRef(newSettings), queries });

    if (defaultQueries) {
      queryRunner.runQueries();
    }

    this.loadDataSource();
  };

  public onQueryOptionsChange = (options: QueryGroupOptions) => {
    const panel = this.state.panelRef.resolve();
    const dataObj = this.queryRunner;

    const dataObjStateUpdate: Partial<SceneQueryRunner['state']> = {};
    const panelStateUpdate: Partial<VizPanel['state']> = {};

    if (options.maxDataPoints !== dataObj.state.maxDataPoints) {
      dataObjStateUpdate.maxDataPoints = options.maxDataPoints ?? undefined;
    }

    if (options.minInterval !== dataObj.state.minInterval) {
      dataObjStateUpdate.minInterval = options.minInterval ?? undefined;
    }

    const timeFrom = options.timeRange?.from ?? undefined;
    const timeShift = options.timeRange?.shift ?? undefined;
    const hideTimeOverride = options.timeRange?.hide;

    if (timeFrom !== undefined || timeShift !== undefined) {
      panelStateUpdate.$timeRange = new PanelTimeRange({ timeFrom, timeShift, hideTimeOverride });
      panelStateUpdate.hoverHeader = getUpdatedHoverHeader(panel.state.title, panelStateUpdate.$timeRange);
    } else {
      panelStateUpdate.$timeRange = undefined;
      panelStateUpdate.hoverHeader = getUpdatedHoverHeader(panel.state.title, undefined);
    }

    if (options.cacheTimeout !== dataObj?.state.cacheTimeout) {
      dataObjStateUpdate.cacheTimeout = options.cacheTimeout;
    }

    if (options.queryCachingTTL !== dataObj?.state.queryCachingTTL) {
      dataObjStateUpdate.queryCachingTTL = options.queryCachingTTL;
    }

    panel.setState(panelStateUpdate);

    dataObj.setState(dataObjStateUpdate);
    dataObj.runQueries();
  };

  public onQueriesChange = (queries: SceneDataQuery[]) => {
    const runner = this.queryRunner;
    runner.setState({ queries });
  };

  public onRunQueries = () => {
    this.queryRunner.runQueries();
  };

  public getQueries() {
    return this.queryRunner.state.queries;
  }

  public newQuery(): Partial<DataQuery> {
    const { dsSettings, datasource } = this.state;
    let ds;

    if (!dsSettings?.meta.mixed) {
      ds = dsSettings; // Use dsSettings if it is not mixed
    } else if (!datasource?.meta.mixed) {
      ds = datasource; // Use datasource if dsSettings is mixed but datasource is not
    } else {
      // Use default datasource if both are mixed or just datasource is mixed
      ds = getDataSourceSrv().getInstanceSettings(config.defaultDatasource);
    }

    return {
      ...datasource?.getDefaultQuery?.(CoreApp.PanelEditor),
      datasource: { uid: ds?.uid, type: ds?.type },
    };
  }

  public addQueryClick = () => {
    const queries = this.getQueries();
    this.onQueriesChange(addQuery(queries, this.newQuery()));
  };

  public onAddQuery = (query: Partial<DataQuery>) => {
    const queries = this.getQueries();
    const dsSettings = this.state.dsSettings;

    this.onQueriesChange(
      addQuery(queries, query, dsSettings ? getDataSourceRef(dsSettings) : { type: undefined, uid: undefined })
    );
  };

  public isExpressionsSupported(dsSettings: DataSourceInstanceSettings): boolean {
    return (dsSettings.meta.alerting || dsSettings.meta.mixed) === true;
  }

  public onAddExpressionClick = () => {
    const queries = this.getQueries();
    this.onQueriesChange(addQuery(queries, expressionDatasource.newQuery()));
  };

  public renderExtraActions() {
    return GroupActionComponents.getAllExtraRenderAction()
      .map((action, index) =>
        action({
          onAddQuery: this.onAddQuery,
          onChangeDataSource: this.onChangeDataSource,
          key: index,
        })
      )
      .filter(Boolean);
  }

  public get queryRunner(): SceneQueryRunner {
    return getQueryRunnerFor(this.state.panelRef.resolve())!;
  }
}

export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<PanelDataQueriesTab>) {
  const { datasource, dsSettings } = model.useState();
  const { data, queries } = model.queryRunner.useState();
  const { openDrawer: openQueryLibraryDrawer, queryLibraryEnabled } = useQueryLibraryContext();

  if (!datasource || !dsSettings || !data) {
    return null;
  }
  const showAddButton = !isSharedDashboardQuery(dsSettings.name);
  const onSelectQueryFromLibrary = async (query: DataQuery) => {
    // ensure all queries explicitly define a datasource
    const enrichedQueries = queries.map((q) =>
      q.datasource
        ? q
        : {
            ...q,
            datasource: datasource.getRef(),
          }
    );
    const newQueries = addQuery(enrichedQueries, query);
    model.onQueriesChange(newQueries);
    if (query.datasource?.uid) {
      const uniqueDatasources = new Set(
        newQueries.map((q) => q.datasource?.uid).filter((uid) => uid !== ExpressionDatasourceUID)
      );
      const isMixed = uniqueDatasources.size > 1;
      const newDatasourceRef = {
        uid: isMixed ? MIXED_DATASOURCE_NAME : query.datasource.uid,
      };
      const shouldChangeDatasource = datasource.uid !== newDatasourceRef.uid;
      if (shouldChangeDatasource) {
        const newDatasource = getDatasourceSrv().getInstanceSettings(newDatasourceRef);
        if (newDatasource) {
          await model.onChangeDataSource(newDatasource);
        }
      }
    }
  };

  return (
    <div data-testid={selectors.components.QueryTab.content}>
      <QueryGroupTopSection
        data={data}
        dsSettings={dsSettings}
        dataSource={datasource}
        options={model.buildQueryOptions()}
        onDataSourceChange={model.onChangeDataSource}
        onOptionsChange={model.onQueryOptionsChange}
        onOpenQueryInspector={model.onOpenInspector}
      />

      <QueryEditorRows
        data={data}
        queries={queries}
        dsSettings={dsSettings}
        onAddQuery={model.onAddQuery}
        onQueriesChange={model.onQueriesChange}
        onRunQueries={model.onRunQueries}
      />

      <Stack gap={2}>
        {showAddButton && (
          <>
            <Button
              icon="plus"
              onClick={model.addQueryClick}
              variant="secondary"
              data-testid={selectors.components.QueryTab.addQuery}
            >
              Add query
            </Button>
            {queryLibraryEnabled && (
              <Button
                icon="plus"
                onClick={() =>
                  openQueryLibraryDrawer(getDatasourceNames(datasource, queries), onSelectQueryFromLibrary)
                }
                variant="secondary"
                data-testid={selectors.components.QueryTab.addQuery}
              >
                <Trans i18nKey={'dashboards.panel-queries.add-query-from-library'}>Add query from library</Trans>
              </Button>
            )}
          </>
        )}
        {config.expressionsEnabled && model.isExpressionsSupported(dsSettings) && (
          <Button
            icon="plus"
            onClick={model.onAddExpressionClick}
            variant="secondary"
            data-testid={selectors.components.QueryTab.addExpression}
          >
            <span>Expression&nbsp;</span>
          </Button>
        )}
        {model.renderExtraActions()}
      </Stack>
    </div>
  );
}

function getDatasourceNames(datasource: DataSourceApi, queries: DataQuery[]): string[] {
  if (datasource.uid === '-- Mixed --') {
    // If datasource is mixed, the datasource UID is on the query. Here we map the UIDs to datasource names.
    const dsSrv = getDataSourceSrv();
    return queries.map((ds) => dsSrv.getInstanceSettings(ds.datasource)?.name).filter((name) => name !== undefined);
  } else {
    return [datasource.name];
  }
}

interface QueriesTabProps extends PanelDataTabHeaderProps {
  model: PanelDataQueriesTab;
}

function QueriesTab(props: QueriesTabProps) {
  const { model } = props;

  const queryRunnerState = model.queryRunner.useState();

  return (
    <Tab
      label={model.getTabLabel()}
      icon="database"
      counter={queryRunnerState.queries.length}
      active={props.active}
      onChangeTab={props.onChangeTab}
    />
  );
}
