import { css } from '@emotion/css';
import React, { useMemo } from 'react';
import { useAsync } from 'react-use';

import { GrafanaTheme2, TypedVariableModel } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { InlineLabel, SegmentSection, useStyles2 } from '@grafana/ui';

import InfluxDatasource from '../../datasource';
import {
  getAllMeasurementsForTags,
  getAllPolicies,
  getFieldKeysForMeasurement,
  getTagKeysForMeasurementAndTags,
  getTagValues,
} from '../../influxQLMetadataQuery';
import {
  addNewGroupByPart,
  addNewSelectPart,
  changeGroupByPart,
  changeSelectPart,
  normalizeQuery,
  removeGroupByPart,
  removeSelectPart,
} from '../../queryUtils';
import { InfluxQuery, InfluxQueryTag } from '../../types';
import { DEFAULT_RESULT_FORMAT } from '../constants';
import { useUniqueId } from '../useUniqueId';

import { FormatAsSection } from './FormatAsSection';
import { FromSection } from './FromSection';
import { InputSection } from './InputSection';
import { OrderByTimeSection } from './OrderByTimeSection';
import { PartListSection } from './PartListSection';
import { TagsSection } from './TagsSection';
import { getNewGroupByPartOptions, getNewSelectPartOptions, makePartList } from './partListUtils';

type Props = {
  query: InfluxQuery;
  onChange: (query: InfluxQuery) => void;
  onRunQuery: () => void;
  datasource: InfluxDatasource;
};

function wrapRegex(v: TypedVariableModel): string {
  return `/^$${v.name}$/`;
}

function wrapPure(v: TypedVariableModel): string {
  return `$${v.name}`;
}

function getTemplateVariableOptions(wrapper: (v: TypedVariableModel) => string) {
  return (
    getTemplateSrv()
      .getVariables()
      // we make them regex-params, i'm not 100% sure why.
      // probably because this way multi-value variables work ok too.
      .map(wrapper)
  );
}

// helper function to make it easy to call this from the widget-render-code
function withTemplateVariableOptions(
  optionsPromise: Promise<string[]>,
  wrapper: (v: TypedVariableModel) => string,
  filter?: string
): Promise<string[]> {
  let templateVariableOptions = getTemplateVariableOptions(wrapper);
  if (filter) {
    templateVariableOptions = templateVariableOptions.filter((tvo) => tvo.indexOf(filter) > -1);
  }
  return optionsPromise.then((options) => [...templateVariableOptions, ...options]);
}

// it is possible to add fields into the `InfluxQueryTag` structures, and they do work,
// but in some cases, when we do metadata queries, we have to remove them from the queries.
function filterTags(parts: InfluxQueryTag[], allTagKeys: Set<string>): InfluxQueryTag[] {
  return parts.filter((t) => t.key.endsWith('::tag') || allTagKeys.has(t.key + '::tag'));
}

export const Editor = (props: Props): JSX.Element => {
  const uniqueId = useUniqueId();
  const formatAsId = `influxdb-qe-format-as-${uniqueId}`;
  const orderByTimeId = `influxdb-qe-order-by${uniqueId}`;

  const styles = useStyles2(getStyles);
  const query = normalizeQuery(props.query);
  const { datasource } = props;
  const { measurement, policy } = query;

  const policyData = useAsync(() => getAllPolicies(datasource), [datasource]);
  const retentionPolicies = !!policyData.error ? [] : policyData.value ?? [];

  const allTagKeys = useMemo(async () => {
    const tagKeys = (await getTagKeysForMeasurementAndTags(measurement, policy, [], datasource)).map(
      (tag) => `${tag}::tag`
    );

    const fieldKeys = (await getFieldKeysForMeasurement(measurement || '', policy, datasource)).map(
      (field) => `${field}::field`
    );

    return new Set([...tagKeys, ...fieldKeys]);
  }, [measurement, policy, datasource]);

  const selectLists = useMemo(() => {
    const dynamicSelectPartOptions = new Map([
      [
        'field_0',
        () => {
          return measurement !== undefined
            ? getFieldKeysForMeasurement(measurement, policy, datasource)
            : Promise.resolve([]);
        },
      ],
    ]);
    return (query.select ?? []).map((sel) => makePartList(sel, dynamicSelectPartOptions));
  }, [measurement, policy, query.select, datasource]);

  // the following function is not complicated enough to memoize, but it's result
  // is used in both memoized and un-memoized parts, so we have no choice
  const getTagKeys = useMemo(
    () => async () => {
      const selectedTagKeys = new Set(query.tags?.map((tag) => tag.key));

      return [...(await allTagKeys)].filter((tagKey) => !selectedTagKeys.has(tagKey));
    },
    [query.tags, allTagKeys]
  );

  const groupByList = useMemo(() => {
    const dynamicGroupByPartOptions = new Map([['tag_0', getTagKeys]]);

    return makePartList(query.groupBy ?? [], dynamicGroupByPartOptions);
  }, [getTagKeys, query.groupBy]);

  const onAppliedChange = (newQuery: InfluxQuery) => {
    props.onChange(newQuery);
    props.onRunQuery();
  };
  const handleFromSectionChange = (p: string | undefined, m: string | undefined) => {
    onAppliedChange({
      ...query,
      policy: p,
      measurement: m,
    });
  };

  const handleTagsSectionChange = (tags: InfluxQueryTag[]) => {
    // we set empty-arrays to undefined
    onAppliedChange({
      ...query,
      tags: tags.length === 0 ? undefined : tags,
    });
  };

  return (
    <div>
      <SegmentSection label="FROM" fill={true}>
        <FromSection
          policy={policy ?? retentionPolicies[0]}
          measurement={measurement}
          getPolicyOptions={() =>
            withTemplateVariableOptions(
              allTagKeys.then((keys) => getAllPolicies(datasource)),
              wrapPure
            )
          }
          getMeasurementOptions={(filter) =>
            withTemplateVariableOptions(
              allTagKeys.then((keys) =>
                getAllMeasurementsForTags(
                  filter === '' ? undefined : filter,
                  filterTags(query.tags ?? [], keys),
                  datasource
                )
              ),
              wrapRegex,
              filter
            )
          }
          onChange={handleFromSectionChange}
        />
        <InlineLabel width="auto" className={styles.inlineLabel}>
          WHERE
        </InlineLabel>
        <TagsSection
          tags={query.tags ?? []}
          onChange={handleTagsSectionChange}
          getTagKeyOptions={getTagKeys}
          getTagValueOptions={(key: string) =>
            withTemplateVariableOptions(
              allTagKeys.then((keys) =>
                getTagValues(key, measurement, policy, filterTags(query.tags ?? [], keys), datasource)
              ),
              wrapRegex
            )
          }
        />
      </SegmentSection>
      {selectLists.map((sel, index) => (
        <SegmentSection key={index} label={index === 0 ? 'SELECT' : ''} fill={true}>
          <PartListSection
            parts={sel}
            getNewPartOptions={() => Promise.resolve(getNewSelectPartOptions())}
            onChange={(partIndex, newParams) => {
              const newQuery = changeSelectPart(query, index, partIndex, newParams);
              onAppliedChange(newQuery);
            }}
            onAddNewPart={(type) => {
              onAppliedChange(addNewSelectPart(query, type, index));
            }}
            onRemovePart={(partIndex) => {
              onAppliedChange(removeSelectPart(query, partIndex, index));
            }}
          />
        </SegmentSection>
      ))}
      <SegmentSection label="GROUP BY" fill={true}>
        <PartListSection
          parts={groupByList}
          getNewPartOptions={() => getNewGroupByPartOptions(query, getTagKeys)}
          onChange={(partIndex, newParams) => {
            const newQuery = changeGroupByPart(query, partIndex, newParams);
            onAppliedChange(newQuery);
          }}
          onAddNewPart={(type) => {
            onAppliedChange(addNewGroupByPart(query, type));
          }}
          onRemovePart={(partIndex) => {
            onAppliedChange(removeGroupByPart(query, partIndex));
          }}
        />
      </SegmentSection>
      <SegmentSection label="TIMEZONE" fill={true}>
        <InputSection
          placeholder="(optional)"
          value={query.tz}
          onChange={(tz) => {
            onAppliedChange({ ...query, tz });
          }}
        />
        <InlineLabel htmlFor={orderByTimeId} width="auto" className={styles.inlineLabel}>
          ORDER BY TIME
        </InlineLabel>
        <OrderByTimeSection
          inputId={orderByTimeId}
          value={query.orderByTime === 'DESC' ? 'DESC' : 'ASC' /* FIXME: make this shared with influx_query_model */}
          onChange={(v) => {
            onAppliedChange({ ...query, orderByTime: v });
          }}
        />
      </SegmentSection>
      {/* query.fill is ignored in the query-editor, and it is deleted whenever
          query-editor changes. the influx_query_model still handles it, but the new
          approach seem to be to handle "fill" inside query.groupBy. so, if you
          have a panel where in the json you have query.fill, it will be applied,
          as long as you do not edit that query. */}
      <SegmentSection label="LIMIT" fill={true}>
        <InputSection
          placeholder="(optional)"
          value={query.limit?.toString()}
          onChange={(limit) => {
            onAppliedChange({ ...query, limit });
          }}
        />
        <InlineLabel width="auto" className={styles.inlineLabel}>
          SLIMIT
        </InlineLabel>
        <InputSection
          placeholder="(optional)"
          value={query.slimit?.toString()}
          onChange={(slimit) => {
            onAppliedChange({ ...query, slimit });
          }}
        />
      </SegmentSection>
      <SegmentSection htmlFor={formatAsId} label="FORMAT AS" fill={true}>
        <FormatAsSection
          inputId={formatAsId}
          format={query.resultFormat ?? DEFAULT_RESULT_FORMAT}
          onChange={(format) => {
            onAppliedChange({ ...query, resultFormat: format });
          }}
        />
        {query.resultFormat !== 'table' && (
          <>
            <InlineLabel width="auto" className={styles.inlineLabel}>
              ALIAS
            </InlineLabel>
            <InputSection
              isWide
              placeholder="Naming pattern"
              value={query.alias}
              onChange={(alias) => {
                onAppliedChange({ ...query, alias });
              }}
            />
          </>
        )}
      </SegmentSection>
    </div>
  );
};

function getStyles(theme: GrafanaTheme2) {
  return {
    inlineLabel: css`
      color: ${theme.colors.primary.text};
    `,
  };
}
