import classNames from 'classnames';
import { cloneDeep, filter, uniqBy, uniqueId } from 'lodash';
import pluralize from 'pluralize';
import { PureComponent, type ReactNode, type JSX, createRef } from 'react';

import {
  CoreApp,
  type DataSourceApi,
  type DataSourceInstanceSettings,
  DataSourcePluginContextProvider,
  type PluginExtensionQueryEditorRowAdaptiveTelemetryV1Context,
  type EventBusExtended,
  type HistoryItem,
  LoadingState,
  type PanelData,
  type QueryResultMetaNotice,
  type TimeRange,
  getDataSourceRef,
  PluginExtensionPoints,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { getDataSourceSrv, renderLimitedComponents, reportInteraction, usePluginComponents } from '@grafana/runtime';
import { type DataQuery } from '@grafana/schema';
import { Badge, ErrorBoundaryAlert, List } from '@grafana/ui';
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
import {
  QueryOperationAction,
  QueryOperationToggleAction,
} from 'app/core/components/QueryOperationRow/QueryOperationAction';
import {
  QueryOperationRow,
  type QueryOperationRowRenderProps,
} from 'app/core/components/QueryOperationRow/QueryOperationRow';

import { useQueryLibraryContext } from '../../explore/QueryLibrary/QueryLibraryContext';
import { ExpressionDatasourceUID } from '../../expressions/types';

import { type QueryActionComponent, RowActionComponents } from './QueryActionComponent';
import { QueryEditorRowHeader } from './QueryEditorRowHeader';
import { QueryErrorAlert } from './QueryErrorAlert';
import { QueryLibraryEditingContainer } from './QueryLibraryEditingContainer';

export interface Props<TQuery extends DataQuery> {
  data: PanelData;
  query: TQuery;
  queries: TQuery[];
  id: string;
  index: number;
  dataSource: DataSourceInstanceSettings;
  onChangeDataSource?: (dsSettings: DataSourceInstanceSettings) => void;
  onDataSourceLoaded?: (instance: DataSourceApi) => void;
  renderHeaderExtras?: () => ReactNode;
  onAddQuery: (query: TQuery) => void;
  onRemoveQuery: (query: TQuery) => void;
  onChange: (query: TQuery) => void;
  onReplace?: (query: DataQuery) => void;
  onRunQuery: () => void;
  visualization?: ReactNode;
  hideHideQueryButton?: boolean;
  app?: CoreApp;
  range: TimeRange;
  history?: Array<HistoryItem<TQuery>>;
  eventBus?: EventBusExtended;
  hideActionButtons?: boolean;
  onQueryCopied?: () => void;
  onQueryRemoved?: () => void;
  onQueryToggled?: (queryStatus?: boolean | undefined) => void;
  onQueryOpenChanged?: (status?: boolean | undefined) => void;
  onQueryReplacedFromLibrary?: () => void;
  collapsable?: boolean;
  hideRefId?: boolean;
  queryLibraryRef?: string;
  onCancelQueryLibraryEdit?: () => void;
  isOpen?: boolean;
}

interface State<TQuery extends DataQuery> {
  /** DatasourceUid or ds variable expression used to resolve current datasource */
  queriedDataSourceIdentifier?: string | null;
  datasource: DataSourceApi<TQuery> | null;
  datasourceUid?: string | null;
  data?: PanelData;
  isOpen?: boolean;
  showingHelp: boolean;
}

export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Props<TQuery>, State<TQuery>> {
  dataSourceSrv = getDataSourceSrv();
  id = '';
  editorRef = createRef<HTMLDivElement>();

  state: State<TQuery> = {
    datasource: null,
    data: undefined,
    isOpen: true,
    showingHelp: false,
  };

  componentDidMount() {
    const { data, query, id } = this.props;
    const dataFilteredByRefId = filterPanelDataToQuery(data, query.refId);
    this.id = uniqueId(id + '_');
    this.setState({ data: dataFilteredByRefId });

    this.loadDatasource();
  }

  /**
   * When datasource variables are used the query.datasource.uid property is a string variable expression
   * DataSourceSettings.uid can also be this variable expression.
   * This function always returns the current interpolated datasource uid.
   */
  getInterpolatedDataSourceUID(): string | undefined {
    if (this.props.query.datasource) {
      const instanceSettings = this.dataSourceSrv.getInstanceSettings(this.props.query.datasource);
      return instanceSettings?.rawRef?.uid ?? instanceSettings?.uid;
    }

    return this.props.dataSource.rawRef?.uid ?? this.props.dataSource.uid;
  }

  async loadDatasource() {
    let datasource: DataSourceApi;
    const interpolatedUID = this.getInterpolatedDataSourceUID();

    try {
      datasource = await this.dataSourceSrv.get(interpolatedUID);
    } catch (error) {
      // If the DS doesn't exist, it fails. Getting with no args returns the default DS.
      datasource = await this.dataSourceSrv.get();
    }

    if (typeof this.props.onDataSourceLoaded === 'function') {
      this.props.onDataSourceLoaded(datasource);
    }

    this.setState({
      datasource: datasource as unknown as DataSourceApi<TQuery>,
      queriedDataSourceIdentifier: interpolatedUID,
    });
  }

  componentDidUpdate(prevProps: Props<TQuery>) {
    const { datasource, queriedDataSourceIdentifier } = this.state;
    const { data, query } = this.props;

    if (prevProps.id !== this.props.id) {
      this.id = uniqueId(this.props.id + '_');
    }

    if (data !== prevProps.data) {
      const dataFilteredByRefId = filterPanelDataToQuery(data, query.refId);

      this.setState({ data: dataFilteredByRefId });
    }

    // check if we need to load another datasource
    if (datasource && queriedDataSourceIdentifier !== this.getInterpolatedDataSourceUID()) {
      this.loadDatasource();
      return;
    }
  }

  getQueryEditor(ds: DataSourceApi<TQuery>) {
    if (!ds) {
      return;
    }

    switch (this.props.app) {
      case CoreApp.Explore:
        return (
          ds.components?.ExploreMetricsQueryField ||
          ds.components?.ExploreLogsQueryField ||
          ds.components?.ExploreQueryField ||
          ds.components?.QueryEditor
        );
      case CoreApp.PanelEditor:
      case CoreApp.Dashboard:
      default:
        return ds.components?.QueryEditor;
    }
  }

  isWaitingForDatasourceToLoad(): boolean {
    // if we not yet have loaded the datasource in state the
    // ds in props and the ds in state will have different values.
    return this.getInterpolatedDataSourceUID() !== this.state.queriedDataSourceIdentifier;
  }

  renderPluginEditor = () => {
    const { query, onChange, queries, onRunQuery, onAddQuery, range, app = CoreApp.PanelEditor, history } = this.props;
    const { datasource, data } = this.state;

    if (this.isWaitingForDatasourceToLoad()) {
      return null;
    }

    if (datasource) {
      let QueryEditor = this.getQueryEditor(datasource);

      if (QueryEditor) {
        return (
          <DataSourcePluginContextProvider instanceSettings={this.props.dataSource}>
            <QueryEditor
              key={datasource?.name}
              query={query}
              datasource={datasource}
              onChange={onChange}
              onRunQuery={onRunQuery}
              onAddQuery={onAddQuery}
              data={data}
              range={range}
              queries={queries}
              app={app}
              history={history}
            />
          </DataSourcePluginContextProvider>
        );
      }
    }

    return (
      <div>
        <Trans i18nKey="query-operation.query-editor-not-exported">
          Data source plugin does not export any Query Editor component
        </Trans>
      </div>
    );
  };

  onRemoveQuery = () => {
    const { onRemoveQuery, query, onQueryRemoved } = this.props;

    // Track expression query removal
    const isExpressionQuery = query.datasource?.uid === ExpressionDatasourceUID;
    if (isExpressionQuery && 'type' in query && query.type) {
      reportInteraction('dashboards_expression_interaction', {
        action: 'remove_expression',
        expression_type: query.type,
        context: 'panel_query_section',
      });
    }

    onRemoveQuery(query);

    if (onQueryRemoved) {
      onQueryRemoved();
    }
  };

  onCancelQueryLibraryEdit = () => {
    const { query } = this.props;
    reportInteraction('query_library-update_query_from_explore_cancelled', {
      datasourceType: query.datasource?.type,
    });
    this.props.onCancelQueryLibraryEdit?.();
  };

  onExitQueryLibraryEditingMode = () => {
    // Exit query library editing mode after successful update
    this.props.onCancelQueryLibraryEdit?.();
  };

  onCopyQuery = () => {
    const { query, onAddQuery, onQueryCopied } = this.props;
    const copy = cloneDeep(query);
    onAddQuery(copy);

    if (onQueryCopied) {
      onQueryCopied();
    }
  };

  onHideQuery = () => {
    const { query, onChange, onRunQuery, onQueryToggled } = this.props;
    onChange({ ...query, hide: !query.hide });
    onRunQuery();

    if (onQueryToggled) {
      onQueryToggled(query.hide);
    }

    reportInteraction('query_editor_row_hide_query_clicked', {
      hide: !query.hide,
    });
  };

  onToggleHelp = () => {
    this.setState((state) => ({
      showingHelp: !state.showingHelp,
    }));
  };

  onClickExample = (query: TQuery) => {
    if (query.datasource === undefined) {
      query.datasource = getDataSourceRef(this.props.dataSource);
    }

    this.props.onChange({
      ...query,
      refId: this.props.query.refId,
    });
    this.onToggleHelp();
  };

  onSelectQueryFromLibrary = (query: DataQuery) => {
    this.props.onQueryReplacedFromLibrary?.();
    this.props.onReplace?.(query);
  };

  renderCollapsedText(): string | null {
    const { datasource } = this.state;

    if (!datasource || typeof datasource.getQueryDisplayText !== 'function') {
      return null;
    }

    try {
      return datasource.getQueryDisplayText(this.props.query);
    } catch (error) {
      // Some datasource plugins may throw errors in getQueryDisplayText
      // Return null gracefully to prevent the query editor from crashing.
      return null;
    }
  }

  renderWarnings = (type: string): JSX.Element | null => {
    const { data, query } = this.props;
    const dataFilteredByRefId = filterPanelDataToQuery(data, query.refId)?.series ?? [];

    const allWarnings = dataFilteredByRefId.reduce((acc: QueryResultMetaNotice[], serie) => {
      if (!serie.meta?.notices) {
        return acc;
      }

      const warnings = filter(serie.meta.notices, (item: QueryResultMetaNotice) => item.severity === type) ?? [];
      return acc.concat(warnings);
    }, []);

    const uniqueWarnings = uniqBy(allWarnings, 'text');

    const hasWarnings = uniqueWarnings.length > 0;
    if (!hasWarnings) {
      return null;
    }

    const key = 'query-' + type + 's';
    const colour = type === 'warning' ? 'orange' : 'blue';
    const iconName = type === 'warning' ? 'exclamation-triangle' : 'file-landscape-alt';

    const listItems = uniqueWarnings.map((warning) => warning.text);
    const serializedWarnings = <List items={listItems} renderItem={(item) => <>{item}</>} />;

    return (
      <Badge
        key={key}
        color={colour}
        icon={iconName}
        text={
          <>
            {uniqueWarnings.length} {pluralize(type, uniqueWarnings.length)}
          </>
        }
        tooltip={serializedWarnings}
      />
    );
  };

  renderExtraActions = () => {
    const { query, queries, data, onAddQuery, dataSource, app } = this.props;

    const unscopedActions = RowActionComponents.getAllExtraRenderAction();

    let scopedActions: QueryActionComponent[] = [];

    if (app !== undefined) {
      scopedActions = RowActionComponents.getScopedExtraRenderAction(app);
    }

    const extraActions = [...unscopedActions, ...scopedActions]
      .map((action, index) =>
        action({
          query,
          queries,
          timeRange: data.timeRange,
          onAddQuery: onAddQuery as (query: DataQuery) => void,
          dataSource,
          key: index,
        })
      )
      .filter(Boolean);

    extraActions.push(this.renderWarnings('info'));
    extraActions.push(this.renderWarnings('warning'));
    extraActions.push(<AdaptiveTelemetryQueryActions key="adaptive-telemetry-actions" query={query} />);

    return extraActions;
  };

  renderActions = (props: QueryOperationRowRenderProps) => {
    const { query, hideHideQueryButton: hideHideQueryButton = false, queryLibraryRef, app } = this.props;
    const { datasource, showingHelp } = this.state;
    const isHidden = !!query.hide;

    const hasEditorHelp = datasource?.components?.QueryEditorHelp;
    const isEditingQueryLibrary = queryLibraryRef !== undefined;
    const isUnifiedAlerting = app === CoreApp.UnifiedAlerting;
    const isExpressionQuery = query.datasource?.uid === ExpressionDatasourceUID;

    return (
      <>
        {!isEditingQueryLibrary && !isUnifiedAlerting && !isExpressionQuery && (
          <SavedQueryButtons
            query={{
              ...query,
              datasource: datasource ? { uid: datasource.uid, type: datasource.type } : query.datasource,
            }}
            app={app}
            onUpdateSuccess={this.onExitQueryLibraryEditingMode}
            onSelectQuery={this.onSelectQueryFromLibrary}
            datasourceFilters={datasource?.name ? [datasource.name] : []}
            parentRef={this.editorRef}
          />
        )}

        {hasEditorHelp && (
          <QueryOperationToggleAction
            title={t('query-operation.header.datasource-help', 'Show data source help')}
            icon="question-circle"
            onClick={this.onToggleHelp}
            active={showingHelp}
          />
        )}
        {this.renderExtraActions()}
        {!isEditingQueryLibrary && (
          <QueryOperationAction
            title={t('query-operation.header.duplicate-query', 'Duplicate query')}
            icon="copy"
            onClick={this.onCopyQuery}
          />
        )}

        {!hideHideQueryButton ? (
          <QueryOperationToggleAction
            dataTestId={selectors.components.QueryEditorRow.actionButton('Hide response')}
            title={
              query.hide
                ? t('query-operation.header.show-response', 'Show response')
                : t('query-operation.header.hide-response', 'Hide response')
            }
            icon={isHidden ? 'eye-slash' : 'eye'}
            active={isHidden}
            onClick={this.onHideQuery}
          />
        ) : null}
        {!isEditingQueryLibrary && (
          <QueryOperationAction
            title={t('query-operation.header.remove-query', 'Remove query')}
            icon="trash-alt"
            onClick={this.onRemoveQuery}
          />
        )}
      </>
    );
  };

  renderHeader = (props: QueryOperationRowRenderProps) => {
    const { app, query, dataSource, onChangeDataSource, onChange, queries, renderHeaderExtras, hideRefId } = this.props;

    return (
      <QueryEditorRowHeader
        query={query}
        queries={queries}
        onChangeDataSource={onChangeDataSource}
        dataSource={dataSource}
        hidden={query.hide}
        onChange={onChange}
        collapsedText={!props.isOpen ? this.renderCollapsedText() : null}
        renderExtras={() => <>{renderHeaderExtras && renderHeaderExtras()}</>}
        alerting={app === CoreApp.UnifiedAlerting}
        hideRefId={hideRefId}
      />
    );
  };

  render() {
    const {
      query,
      index,
      visualization,
      collapsable,
      hideActionButtons,
      isOpen,
      onQueryOpenChanged,
      app,
      queryLibraryRef,
      onCancelQueryLibraryEdit,
    } = this.props;
    const { datasource, showingHelp, data } = this.state;
    const isHidden = query.hide;
    const error =
      data?.error && data.error.refId === query.refId ? data.error : data?.errors?.find((e) => e.refId === query.refId);
    const rowClasses = classNames('query-editor-row', {
      'query-editor-row--disabled': isHidden,
      'gf-form-disabled': isHidden,
    });

    if (!datasource) {
      return null;
    }

    const editor = this.renderPluginEditor();
    const DatasourceCheatsheet = datasource.components?.QueryEditorHelp;

    const queryOperationRow = (
      <QueryOperationRow
        id={this.id}
        draggable={!hideActionButtons && !queryLibraryRef}
        collapsable={collapsable}
        index={index}
        headerElement={this.renderHeader}
        actions={hideActionButtons ? undefined : this.renderActions}
        isOpen={isOpen}
        onOpen={onQueryOpenChanged}
      >
        <div className={rowClasses} id={this.id}>
          <ErrorBoundaryAlert boundaryName="query-editor-operation-row">
            {showingHelp && DatasourceCheatsheet && (
              <OperationRowHelp>
                <DatasourceCheatsheet
                  onClickExample={(query) => this.onClickExample(query)}
                  query={this.props.query}
                  datasource={datasource}
                />
              </OperationRowHelp>
            )}
            {editor}
          </ErrorBoundaryAlert>
          {error && <QueryErrorAlert error={error} query={query} />}
          {visualization}
        </div>
      </QueryOperationRow>
    );

    return (
      <div data-testid="query-editor-row" aria-label={selectors.components.QueryEditorRows.rows} ref={this.editorRef}>
        {queryLibraryRef && (
          <MaybeQueryLibraryEditingHeader
            query={query}
            app={app}
            queryLibraryRef={queryLibraryRef}
            onCancelEdit={onCancelQueryLibraryEdit}
            onUpdateSuccess={this.onExitQueryLibraryEditingMode}
            onSelectQuery={this.onSelectQueryFromLibrary}
          />
        )}
        {queryLibraryRef ? (
          <QueryLibraryEditingContainer>{queryOperationRow}</QueryLibraryEditingContainer>
        ) : (
          queryOperationRow
        )}
      </div>
    );
  }
}

/**
 * Get a version of the PanelData limited to the query we are looking at
 */
export function filterPanelDataToQuery(data: PanelData, refId: string): PanelData | undefined {
  const series = data.series.filter((series) => series.refId === refId);

  // If there was an error with no data and the panel is not in a loading state, pass it to the QueryEditors
  if (data.state !== LoadingState.Loading && (data.error || data.errors?.length) && !data.series.length) {
    return {
      ...data,
      state: LoadingState.Error,
    };
  }

  // Only say this is an error if the error links to the query
  let state = data.state;
  let error = data.errors?.find((e) => e.refId === refId);
  if (!error && data.error) {
    error = data.error.refId === refId ? data.error : undefined;
  }

  if (state !== LoadingState.Loading) {
    if (error) {
      state = LoadingState.Error;
    } else if (data.state === LoadingState.Error) {
      state = LoadingState.Done;
    }
  }

  const timeRange = data.timeRange;

  return {
    ...data,
    state,
    series,
    error,
    errors: error ? [error] : undefined,
    timeRange,
  };
}

// Will render anything only if saved query is enabled
function SavedQueryButtons(props: {
  query: DataQuery;
  app?: CoreApp;
  onUpdateSuccess?: () => void;
  onSelectQuery: (query: DataQuery) => void;
  datasourceFilters: string[];
  parentRef: React.RefObject<HTMLDivElement | null>;
}) {
  const { renderSavedQueryButtons } = useQueryLibraryContext();
  return renderSavedQueryButtons(
    props.query,
    props.app,
    props.onUpdateSuccess,
    props.onSelectQuery,
    undefined,
    props.parentRef
  );
}

// Will render editing header only if query library is enabled
function MaybeQueryLibraryEditingHeader(props: {
  query: DataQuery;
  app?: CoreApp;
  queryLibraryRef?: string;
  onCancelEdit?: () => void;
  onUpdateSuccess?: () => void;
  onSelectQuery?: (query: DataQuery) => void;
}) {
  const { renderQueryLibraryEditingHeader } = useQueryLibraryContext();
  return renderQueryLibraryEditingHeader(
    props.query,
    props.app,
    props.queryLibraryRef,
    props.onCancelEdit,
    props.onUpdateSuccess,
    props.onSelectQuery
  );
}

function AdaptiveTelemetryQueryActions({ query }: { query: DataQuery }) {
  try {
    const { isLoading, components } = usePluginComponents<PluginExtensionQueryEditorRowAdaptiveTelemetryV1Context>({
      extensionPointId: PluginExtensionPoints.QueryEditorRowAdaptiveTelemetryV1,
    });

    if (isLoading || !components.length) {
      return null;
    }

    return renderLimitedComponents({
      props: { query, contextHints: ['queryeditorrow', 'header'] },
      components,
      limit: 1,
      pluginId: /grafana-adaptive.*/,
    });
  } catch (error) {
    // If `usePluginComponents` isn't properly resolved, tests will fail with 'setPluginComponentsHook(options) can only be used after the Grafana instance has started.'
    // This will be resolved in https://github.com/grafana/grafana/pull/92983
    // In this case, Return `null` like when there are no extensions.
    return null;
  }
}
