import { css, cx } from '@emotion/css';
import { capitalize, uniqueId } from 'lodash';
import React, { FC, useCallback, useState } from 'react';

import { DataFrame, dateTimeFormat, GrafanaTheme2, isTimeSeriesFrames, LoadingState, PanelData } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { AutoSizeInput, Button, clearButtonStyles, Icon, IconButton, Select, useStyles2 } from '@grafana/ui';
import { ClassicConditions } from 'app/features/expressions/components/ClassicConditions';
import { Math } from 'app/features/expressions/components/Math';
import { Reduce } from 'app/features/expressions/components/Reduce';
import { Resample } from 'app/features/expressions/components/Resample';
import { Threshold } from 'app/features/expressions/components/Threshold';
import { ExpressionQuery, ExpressionQueryType, gelTypes } from 'app/features/expressions/types';
import { AlertQuery, PromAlertingRuleState } from 'app/types/unified-alerting-dto';

import { usePagination } from '../../hooks/usePagination';
import { HoverCard } from '../HoverCard';
import { Spacer } from '../Spacer';
import { AlertStateTag } from '../rules/AlertStateTag';

import { AlertConditionIndicator } from './AlertConditionIndicator';
import { formatLabels, getSeriesLabels, getSeriesName, getSeriesValue, isEmptySeries } from './util';

interface ExpressionProps {
  isAlertCondition?: boolean;
  data?: PanelData;
  error?: Error;
  warning?: Error;
  queries: AlertQuery[];
  query: ExpressionQuery;
  onSetCondition: (refId: string) => void;
  onUpdateRefId: (oldRefId: string, newRefId: string) => void;
  onRemoveExpression: (refId: string) => void;
  onUpdateExpressionType: (refId: string, type: ExpressionQueryType) => void;
  onChangeQuery: (query: ExpressionQuery) => void;
}

export const Expression: FC<ExpressionProps> = ({
  queries = [],
  query,
  data,
  error,
  warning,
  isAlertCondition,
  onSetCondition,
  onUpdateRefId,
  onRemoveExpression,
  onUpdateExpressionType,
  onChangeQuery,
}) => {
  const styles = useStyles2(getStyles);

  const queryType = query?.type;

  const isLoading = data && Object.values(data).some((d) => Boolean(d) && d.state === LoadingState.Loading);
  const hasResults = Array.isArray(data?.series) && !isLoading;
  const series = data?.series ?? [];

  const alertCondition = isAlertCondition ?? false;
  const showSummary = isAlertCondition && hasResults;

  const groupedByState = {
    [PromAlertingRuleState.Firing]: series.filter((serie) => getSeriesValue(serie) !== 0),
    [PromAlertingRuleState.Inactive]: series.filter((serie) => getSeriesValue(serie) === 0),
  };

  const renderExpressionType = useCallback(
    (query: ExpressionQuery) => {
      // these are the refs we can choose from that don't include the current one
      const availableRefIds = queries
        .filter((q) => query.refId !== q.refId)
        .map((q) => ({ value: q.refId, label: q.refId }));

      switch (query.type) {
        case ExpressionQueryType.math:
          return <Math onChange={onChangeQuery} query={query} labelWidth={'auto'} onRunQuery={() => {}} />;

        case ExpressionQueryType.reduce:
          return <Reduce onChange={onChangeQuery} refIds={availableRefIds} labelWidth={'auto'} query={query} />;

        case ExpressionQueryType.resample:
          return <Resample onChange={onChangeQuery} query={query} labelWidth={'auto'} refIds={availableRefIds} />;

        case ExpressionQueryType.classic:
          return <ClassicConditions onChange={onChangeQuery} query={query} refIds={availableRefIds} />;

        case ExpressionQueryType.threshold:
          return <Threshold onChange={onChangeQuery} query={query} labelWidth={'auto'} refIds={availableRefIds} />;

        default:
          return <>Expression not supported: {query.type}</>;
      }
    },
    [onChangeQuery, queries]
  );

  return (
    <div className={cx(styles.expression.wrapper, alertCondition && styles.expression.alertCondition)}>
      <div className={styles.expression.stack}>
        <Header
          refId={query.refId}
          queryType={queryType}
          onRemoveExpression={() => onRemoveExpression(query.refId)}
          onUpdateRefId={(newRefId) => onUpdateRefId(query.refId, newRefId)}
          onUpdateExpressionType={(type) => onUpdateExpressionType(query.refId, type)}
        />
        <div className={styles.expression.body}>{renderExpressionType(query)}</div>
        {hasResults && <ExpressionResult series={series} isAlertCondition={isAlertCondition} />}

        <div className={styles.footer}>
          <Stack direction="row" alignItems="center">
            <AlertConditionIndicator
              onSetCondition={() => onSetCondition(query.refId)}
              enabled={alertCondition}
              error={error}
              warning={warning}
            />
            <Spacer />
            {showSummary && (
              <PreviewSummary
                firing={groupedByState[PromAlertingRuleState.Firing].length}
                normal={groupedByState[PromAlertingRuleState.Inactive].length}
              />
            )}
          </Stack>
        </div>
      </div>
    </div>
  );
};

interface ExpressionResultProps {
  series: DataFrame[];
  isAlertCondition?: boolean;
}
export const PAGE_SIZE = 20;
export const ExpressionResult: FC<ExpressionResultProps> = ({ series, isAlertCondition }) => {
  const { pageItems, previousPage, nextPage, numberOfPages, pageStart, pageEnd } = usePagination(series, 1, PAGE_SIZE);
  const styles = useStyles2(getStyles);

  // sometimes we receive results where every value is just "null" when noData occurs
  const emptyResults = isEmptySeries(series);
  const isTimeSeriesResults = !emptyResults && isTimeSeriesFrames(series);

  const shouldShowPagination = numberOfPages > 1;

  return (
    <div className={styles.expression.results}>
      {!emptyResults && isTimeSeriesResults && (
        <div>
          {pageItems.map((frame, index) => (
            <TimeseriesRow
              key={uniqueId()}
              frame={frame}
              index={pageStart + index}
              isAlertCondition={isAlertCondition}
            />
          ))}
        </div>
      )}
      {!emptyResults &&
        !isTimeSeriesResults &&
        pageItems.map((frame, index) => (
          // There's no way to uniquely identify a frame that doesn't cause render bugs :/ (Gilles)
          <FrameRow key={uniqueId()} frame={frame} index={pageStart + index} isAlertCondition={isAlertCondition} />
        ))}
      {emptyResults && <div className={cx(styles.expression.noData, styles.mutedText)}>No data</div>}
      {shouldShowPagination && (
        <div className={styles.pagination.wrapper} data-testid="paginate-expression">
          <Stack>
            <Button
              variant="secondary"
              fill="outline"
              onClick={previousPage}
              icon="angle-left"
              size="sm"
              aria-label="previous-page"
            />
            <Spacer />
            <span className={styles.mutedText}>
              {pageStart} - {pageEnd} of {series.length}
            </span>
            <Spacer />
            <Button
              variant="secondary"
              fill="outline"
              onClick={nextPage}
              icon="angle-right"
              size="sm"
              aria-label="next-page"
            />
          </Stack>
        </div>
      )}
    </div>
  );
};

export const PreviewSummary: FC<{ firing: number; normal: number }> = ({ firing, normal }) => {
  const { mutedText } = useStyles2(getStyles);
  return <span className={mutedText}>{`${firing} firing, ${normal} normal`}</span>;
};

interface HeaderProps {
  refId: string;
  queryType: ExpressionQueryType;
  onUpdateRefId: (refId: string) => void;
  onRemoveExpression: () => void;
  onUpdateExpressionType: (type: ExpressionQueryType) => void;
}

const Header: FC<HeaderProps> = ({ refId, queryType, onUpdateRefId, onUpdateExpressionType, onRemoveExpression }) => {
  const styles = useStyles2(getStyles);
  const clearButton = useStyles2(clearButtonStyles);
  /**
   * There are 3 edit modes:
   *
   * 1. "refId": Editing the refId (ie. A -> B)
   * 2. "epressionType": Editing the type of the expression (ie. Reduce -> Math)
   * 3. "false": This means we're not editing either of those
   */
  const [editMode, setEditMode] = useState<'refId' | 'expressionType' | false>(false);

  const editing = editMode !== false;
  const editingRefId = editing && editMode === 'refId';
  const editingType = editing && editMode === 'expressionType';

  const selectedExpressionType = gelTypes.find((o) => o.value === queryType);

  return (
    <header className={styles.header.wrapper}>
      <Stack direction="row" gap={0.5} alignItems="center">
        <Stack direction="row" gap={1} alignItems="center" wrap={false}>
          {!editingRefId && (
            <button type="button" className={cx(clearButton, styles.editable)} onClick={() => setEditMode('refId')}>
              <div className={styles.expression.refId}>{refId}</div>
            </button>
          )}
          {editingRefId && (
            <AutoSizeInput
              autoFocus
              defaultValue={refId}
              minWidth={5}
              onChange={(event) => {
                onUpdateRefId(event.currentTarget.value);
                setEditMode(false);
              }}
              onFocus={(event) => event.target.select()}
              onBlur={(event) => {
                onUpdateRefId(event.currentTarget.value);
                setEditMode(false);
              }}
            />
          )}
          {!editingType && (
            <button
              type="button"
              className={cx(clearButton, styles.editable)}
              onClick={() => setEditMode('expressionType')}
            >
              <div className={styles.mutedText}>{capitalize(queryType)}</div>
              <Icon size="xs" name="pen" className={styles.mutedIcon} onClick={() => setEditMode('expressionType')} />
            </button>
          )}
          {editingType && (
            <Select
              isOpen
              autoFocus
              onChange={(selection) => {
                onUpdateExpressionType(selection.value ?? ExpressionQueryType.classic);
                setEditMode(false);
              }}
              onBlur={() => {
                setEditMode(false);
              }}
              options={gelTypes}
              value={selectedExpressionType}
              width={25}
            />
          )}
        </Stack>
        <Spacer />
        <IconButton
          type="button"
          name="trash-alt"
          variant="secondary"
          className={styles.mutedIcon}
          onClick={onRemoveExpression}
        />
      </Stack>
    </header>
  );
};

interface FrameProps extends Pick<ExpressionProps, 'isAlertCondition'> {
  frame: DataFrame;
  index: number;
}

const FrameRow: FC<FrameProps> = ({ frame, index, isAlertCondition }) => {
  const styles = useStyles2(getStyles);

  const name = getSeriesName(frame) || 'Series ' + index;
  const value = getSeriesValue(frame);
  const labelsRecord = getSeriesLabels(frame);
  const labels = Object.entries(labelsRecord);
  const hasLabels = labels.length > 0;

  const showFiring = isAlertCondition && value !== 0;
  const showNormal = isAlertCondition && value === 0;

  const title = `${hasLabels ? '' : name}${hasLabels ? `{${formatLabels(labelsRecord)}}` : ''}`;

  return (
    <div className={styles.expression.resultsRow}>
      <Stack direction="row" gap={1} alignItems="center">
        <div className={styles.expression.resultLabel} title={title}>
          <span>{hasLabels ? '' : name}</span>
          {hasLabels && (
            <>
              <span>{'{'}</span>
              {labels.map(([key, value], index) => (
                <span key={uniqueId()}>
                  <span className={styles.expression.labelKey}>{key}</span>
                  <span>=</span>
                  <span>&quot;</span>
                  <span className={styles.expression.labelValue}>{value}</span>
                  <span>&quot;</span>
                  {index < labels.length - 1 && <span>, </span>}
                </span>
              ))}
              <span>{'}'}</span>
            </>
          )}
        </div>
        <div className={styles.expression.resultValue}>{value}</div>
        {showFiring && <AlertStateTag state={PromAlertingRuleState.Firing} size="sm" />}
        {showNormal && <AlertStateTag state={PromAlertingRuleState.Inactive} size="sm" />}
      </Stack>
    </div>
  );
};

const TimeseriesRow: FC<FrameProps & { index: number }> = ({ frame, index }) => {
  const styles = useStyles2(getStyles);

  const valueField = frame.fields[1]; // field 0 is "time", field 1 is "value"

  const hasLabels = valueField.labels;
  const displayNameFromDS = valueField.config?.displayNameFromDS;
  const name = displayNameFromDS ?? (hasLabels ? formatLabels(valueField.labels ?? {}) : 'Series ' + index);

  const timestamps = frame.fields[0].values;

  const getTimestampFromIndex = (index: number) => frame.fields[0].values[index];
  const getValueFromIndex = (index: number) => frame.fields[1].values[index];

  return (
    <div className={styles.expression.resultsRow}>
      <Stack direction="row" gap={1} alignItems="center">
        <span className={cx(styles.mutedText, styles.expression.resultLabel)} title={name}>
          {name}
        </span>
        <div className={styles.expression.resultValue}>
          <HoverCard
            placement="right"
            wrapperClassName={styles.timeseriesTableWrapper}
            content={
              <table className={styles.timeseriesTable}>
                <thead>
                  <tr>
                    <th>Timestamp</th>
                    <th>Value</th>
                  </tr>
                </thead>
                <tbody>
                  {timestamps.map((_, index) => (
                    <tr key={index}>
                      <td className={styles.mutedText}>{dateTimeFormat(getTimestampFromIndex(index))}</td>
                      <td className={styles.expression.resultValue}>{getValueFromIndex(index)}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            }
          >
            <span>Time series data</span>
          </HoverCard>
        </div>
      </Stack>
    </div>
  );
};

const getStyles = (theme: GrafanaTheme2) => ({
  expression: {
    wrapper: css`
      display: flex;
      border: solid 1px ${theme.colors.border.medium};

      border-radius: ${theme.shape.borderRadius()};
      max-width: 640px;
    `,
    stack: css`
      display: flex;
      flex-direction: column;
      flex-wrap: nowrap;
      gap: 0;
      min-width: 0; // this one is important to prevent text overflow
    `,
    alertCondition: css``,
    body: css`
      padding: ${theme.spacing(1)};
      flex: 1;
    `,
    refId: css`
      font-weight: ${theme.typography.fontWeightBold};
      color: ${theme.colors.primary.text};
    `,
    results: css`
      display: flex;
      flex-direction: column;
      flex-wrap: nowrap;

      border-top: solid 1px ${theme.colors.border.medium};
    `,
    noResults: css`
      display: flex;
      align-items: center;
      justify-content: center;
    `,
    resultsRow: css`
      padding: ${theme.spacing(0.75)} ${theme.spacing(1)};

      &:nth-child(odd) {
        background-color: ${theme.colors.background.secondary};
      }

      &:hover {
        background-color: ${theme.colors.background.canvas};
      }
    `,
    labelKey: css`
      color: ${theme.isDark ? '#73bf69' : '#56a64b'};
    `,
    labelValue: css`
      color: ${theme.isDark ? '#ce9178' : '#a31515'};
    `,
    resultValue: css`
      text-align: right;
    `,
    resultLabel: css`
      flex: 1;
      overflow-x: auto;

      display: inline-block;
      white-space: nowrap;
    `,
    noData: css`
      display: flex;
      align-items: center;
      justify-content: center;
      padding: ${theme.spacing()};
    `,
  },
  mutedText: css`
    color: ${theme.colors.text.secondary};
    font-size: 0.9em;

    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  `,
  header: {
    wrapper: css`
      background: ${theme.colors.background.secondary};
      padding: ${theme.spacing(0.5)} ${theme.spacing(1)};
      border-bottom: solid 1px ${theme.colors.border.medium};
    `,
  },
  footer: css`
    background: ${theme.colors.background.secondary};
    padding: ${theme.spacing(1)};
    border-top: solid 1px ${theme.colors.border.medium};
  `,
  draggableIcon: css`
    cursor: grab;
  `,
  mutedIcon: css`
    color: ${theme.colors.text.secondary};
  `,
  editable: css`
    padding: ${theme.spacing(0.5)} ${theme.spacing(1)};
    border: solid 1px ${theme.colors.border.weak};
    border-radius: ${theme.shape.borderRadius()};

    display: flex;
    flex-direction: row;
    align-items: center;
    gap: ${theme.spacing(1)};

    cursor: pointer;
  `,
  timeseriesTableWrapper: css`
    max-height: 500px;

    overflow-y: scroll;
  `,
  timeseriesTable: css`
    table-layout: auto;

    width: 100%;
    height: 100%;

    td,
    th {
      padding: ${theme.spacing(1)};
    }

    td {
      background: ${theme.colors.background.primary};
    }

    th {
      background: ${theme.colors.background.secondary};
    }

    tr {
      border-bottom: 1px solid ${theme.colors.border.medium};

      &:last-of-type {
        border-bottom: none;
      }
    }
  `,
  pagination: {
    wrapper: css`
      border-top: 1px solid ${theme.colors.border.medium};
      padding: ${theme.spacing()};
    `,
  },
});
