import {Fragment, PureComponent} from 'react';
import styled from '@emotion/styled';
import {Location} from 'history';
import capitalize from 'lodash/capitalize';
import isEqual from 'lodash/isEqual';
import maxBy from 'lodash/maxBy';
import minBy from 'lodash/minBy';

import {fetchTotalCount} from 'sentry/actionCreators/events';
import {Client} from 'sentry/api';
import ErrorPanel from 'sentry/components/charts/errorPanel';
import EventsRequest from 'sentry/components/charts/eventsRequest';
import {LineChartSeries} from 'sentry/components/charts/lineChart';
import OptionSelector from 'sentry/components/charts/optionSelector';
import SessionsRequest from 'sentry/components/charts/sessionsRequest';
import {
  ChartControls,
  InlineContainer,
  SectionHeading,
  SectionValue,
} from 'sentry/components/charts/styles';
import LoadingMask from 'sentry/components/loadingMask';
import {PanelAlert} from 'sentry/components/panels';
import Placeholder from 'sentry/components/placeholder';
import {IconWarning} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {
  EventsStats,
  MultiSeriesEventsStats,
  Organization,
  Project,
} from 'sentry/types';
import type {Series} from 'sentry/types/echarts';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {
  getCrashFreeRateSeries,
  MINUTES_THRESHOLD_TO_DISPLAY_SECONDS,
} from 'sentry/utils/sessions';
import withApi from 'sentry/utils/withApi';
import {COMPARISON_DELTA_OPTIONS} from 'sentry/views/alerts/rules/metric/constants';
import {isSessionAggregate, SESSION_AGGREGATE_TO_FIELD} from 'sentry/views/alerts/utils';
import {getComparisonMarkLines} from 'sentry/views/alerts/utils/getComparisonMarkLines';
import {AlertWizardAlertNames} from 'sentry/views/alerts/wizard/options';
import {getAlertTypeFromAggregateDataset} from 'sentry/views/alerts/wizard/utils';

import {
  AlertRuleComparisonType,
  Dataset,
  MetricRule,
  SessionsAggregate,
  TimePeriod,
  TimeWindow,
  Trigger,
} from '../../types';
import {getMetricDatasetQueryExtras} from '../../utils/getMetricDatasetQueryExtras';

import ThresholdsChart from './thresholdsChart';

type Props = {
  aggregate: MetricRule['aggregate'];
  api: Client;
  comparisonType: AlertRuleComparisonType;
  dataset: MetricRule['dataset'];
  environment: string | null;
  handleMEPAlertDataset: (data: EventsStats | MultiSeriesEventsStats | null) => void;
  isQueryValid: boolean;
  location: Location;
  newAlertOrQuery: boolean;
  organization: Organization;
  projects: Project[];
  query: MetricRule['query'];
  resolveThreshold: MetricRule['resolveThreshold'];
  thresholdType: MetricRule['thresholdType'];
  timeWindow: MetricRule['timeWindow'];
  triggers: Trigger[];
  comparisonDelta?: number;
  header?: React.ReactNode;
};

const TIME_PERIOD_MAP: Record<TimePeriod, string> = {
  [TimePeriod.SIX_HOURS]: t('Last 6 hours'),
  [TimePeriod.ONE_DAY]: t('Last 24 hours'),
  [TimePeriod.THREE_DAYS]: t('Last 3 days'),
  [TimePeriod.SEVEN_DAYS]: t('Last 7 days'),
  [TimePeriod.FOURTEEN_DAYS]: t('Last 14 days'),
  [TimePeriod.THIRTY_DAYS]: t('Last 30 days'),
};

/**
 * Just to avoid repeating it
 */
const MOST_TIME_PERIODS: readonly TimePeriod[] = [
  TimePeriod.ONE_DAY,
  TimePeriod.THREE_DAYS,
  TimePeriod.SEVEN_DAYS,
  TimePeriod.FOURTEEN_DAYS,
  TimePeriod.THIRTY_DAYS,
];

/**
 * TimeWindow determines data available in TimePeriod
 * If TimeWindow is small, lower TimePeriod to limit data points
 */
const AVAILABLE_TIME_PERIODS: Record<TimeWindow, readonly TimePeriod[]> = {
  [TimeWindow.ONE_MINUTE]: [
    TimePeriod.SIX_HOURS,
    TimePeriod.ONE_DAY,
    TimePeriod.THREE_DAYS,
    TimePeriod.SEVEN_DAYS,
  ],
  [TimeWindow.FIVE_MINUTES]: MOST_TIME_PERIODS,
  [TimeWindow.TEN_MINUTES]: MOST_TIME_PERIODS,
  [TimeWindow.FIFTEEN_MINUTES]: MOST_TIME_PERIODS,
  [TimeWindow.THIRTY_MINUTES]: MOST_TIME_PERIODS,
  [TimeWindow.ONE_HOUR]: MOST_TIME_PERIODS,
  [TimeWindow.TWO_HOURS]: MOST_TIME_PERIODS,
  [TimeWindow.FOUR_HOURS]: [
    TimePeriod.THREE_DAYS,
    TimePeriod.SEVEN_DAYS,
    TimePeriod.FOURTEEN_DAYS,
    TimePeriod.THIRTY_DAYS,
  ],
  [TimeWindow.ONE_DAY]: [TimePeriod.THIRTY_DAYS],
};

const TIME_WINDOW_TO_SESSION_INTERVAL = {
  [TimeWindow.THIRTY_MINUTES]: '30m',
  [TimeWindow.ONE_HOUR]: '1h',
  [TimeWindow.TWO_HOURS]: '2h',
  [TimeWindow.FOUR_HOURS]: '4h',
  [TimeWindow.ONE_DAY]: '1d',
};

const SESSION_AGGREGATE_TO_HEADING = {
  [SessionsAggregate.CRASH_FREE_SESSIONS]: t('Total Sessions'),
  [SessionsAggregate.CRASH_FREE_USERS]: t('Total Users'),
};

type State = {
  statsPeriod: TimePeriod;
  totalCount: number | null;
};

/**
 * This is a chart to be used in Metric Alert rules that fetches events based on
 * query, timewindow, and aggregations.
 */
class TriggersChart extends PureComponent<Props, State> {
  state: State = {
    statsPeriod: TimePeriod.SEVEN_DAYS,
    totalCount: null,
  };

  componentDidMount() {
    if (!isSessionAggregate(this.props.aggregate)) {
      this.fetchTotalCount();
    }
  }

  componentDidUpdate(prevProps: Props, prevState: State) {
    const {query, environment, timeWindow, aggregate, projects} = this.props;
    const {statsPeriod} = this.state;
    if (
      !isSessionAggregate(aggregate) &&
      (!isEqual(prevProps.projects, projects) ||
        prevProps.environment !== environment ||
        prevProps.query !== query ||
        !isEqual(prevProps.timeWindow, timeWindow) ||
        !isEqual(prevState.statsPeriod, statsPeriod))
    ) {
      this.fetchTotalCount();
    }
  }

  get availableTimePeriods() {
    // We need to special case sessions, because sub-hour windows are available
    // only when time period is six hours or less (backend limitation)
    if (isSessionAggregate(this.props.aggregate)) {
      return {
        ...AVAILABLE_TIME_PERIODS,
        [TimeWindow.THIRTY_MINUTES]: [TimePeriod.SIX_HOURS],
      };
    }

    return AVAILABLE_TIME_PERIODS;
  }

  handleStatsPeriodChange = (timePeriod: string) => {
    this.setState({statsPeriod: timePeriod as TimePeriod});
  };

  getStatsPeriod = () => {
    const {statsPeriod} = this.state;
    const {timeWindow} = this.props;
    const statsPeriodOptions = this.availableTimePeriods[timeWindow];
    const period = statsPeriodOptions.includes(statsPeriod)
      ? statsPeriod
      : statsPeriodOptions[statsPeriodOptions.length - 1];
    return period;
  };

  get comparisonSeriesName() {
    return capitalize(
      COMPARISON_DELTA_OPTIONS.find(({value}) => value === this.props.comparisonDelta)
        ?.label || ''
    );
  }

  async fetchTotalCount() {
    const {
      api,
      organization,
      location,
      newAlertOrQuery,
      environment,
      projects,
      query,
      dataset,
    } = this.props;
    const statsPeriod = this.getStatsPeriod();

    const queryExtras = getMetricDatasetQueryExtras({
      organization,
      location,
      dataset,
      newAlertOrQuery,
    });

    const queryDataset = queryExtras.dataset as undefined | DiscoverDatasets;

    try {
      const totalCount = await fetchTotalCount(api, organization.slug, {
        field: [],
        project: projects.map(({id}) => id),
        query,
        statsPeriod,
        environment: environment ? [environment] : [],
        dataset: queryDataset,
      });
      this.setState({totalCount});
    } catch (e) {
      this.setState({totalCount: null});
    }
  }

  renderChart({
    isLoading,
    isReloading,
    timeseriesData = [],
    comparisonData,
    comparisonMarkLines,
    errorMessage,
    minutesThresholdToDisplaySeconds,
    isQueryValid,
    errored,
    orgFeatures,
  }: {
    isLoading: boolean;
    isQueryValid: boolean;
    isReloading: boolean;
    orgFeatures: string[];
    timeseriesData: Series[];
    comparisonData?: Series[];
    comparisonMarkLines?: LineChartSeries[];
    errorMessage?: string;
    errored?: boolean;
    minutesThresholdToDisplaySeconds?: number;
  }) {
    const {
      triggers,
      resolveThreshold,
      thresholdType,
      header,
      timeWindow,
      aggregate,
      comparisonType,
    } = this.props;
    const {statsPeriod, totalCount} = this.state;
    const statsPeriodOptions = this.availableTimePeriods[timeWindow];
    const period = this.getStatsPeriod();

    const error = orgFeatures.includes('alert-allow-indexed')
      ? errored || errorMessage
      : errored || errorMessage || !isQueryValid;

    return (
      <Fragment>
        {header}
        <TransparentLoadingMask visible={isReloading} />
        {isLoading && !error ? (
          <ChartPlaceholder />
        ) : error ? (
          <ChartErrorWrapper>
            <PanelAlert type="error">
              {!orgFeatures.includes('alert-allow-indexed') && !isQueryValid
                ? t(
                    'Your filter conditions contain an unsupported field - please review.'
                  )
                : typeof errorMessage === 'string'
                ? errorMessage
                : t('An error occurred while fetching data')}
            </PanelAlert>
            <StyledErrorPanel>
              <IconWarning color="gray500" size="lg" />
            </StyledErrorPanel>
          </ChartErrorWrapper>
        ) : (
          <ThresholdsChart
            period={statsPeriod}
            minValue={minBy(timeseriesData[0]?.data, ({value}) => value)?.value}
            maxValue={maxBy(timeseriesData[0]?.data, ({value}) => value)?.value}
            data={timeseriesData}
            comparisonData={comparisonData ?? []}
            comparisonSeriesName={this.comparisonSeriesName}
            comparisonMarkLines={comparisonMarkLines ?? []}
            hideThresholdLines={comparisonType === AlertRuleComparisonType.CHANGE}
            triggers={triggers}
            resolveThreshold={resolveThreshold}
            thresholdType={thresholdType}
            aggregate={aggregate}
            minutesThresholdToDisplaySeconds={minutesThresholdToDisplaySeconds}
          />
        )}
        <ChartControls>
          <InlineContainer data-test-id="alert-total-events">
            <SectionHeading>
              {isSessionAggregate(aggregate)
                ? SESSION_AGGREGATE_TO_HEADING[aggregate]
                : t('Total Events')}
            </SectionHeading>
            <SectionValue>
              {totalCount !== null ? totalCount.toLocaleString() : '\u2014'}
            </SectionValue>
          </InlineContainer>
          <InlineContainer>
            <OptionSelector
              options={statsPeriodOptions.map(timePeriod => ({
                label: TIME_PERIOD_MAP[timePeriod],
                value: timePeriod,
                disabled: isLoading || isReloading,
              }))}
              selected={period}
              onChange={this.handleStatsPeriodChange}
              title={t('Display')}
            />
          </InlineContainer>
        </ChartControls>
      </Fragment>
    );
  }

  render() {
    const {
      api,
      organization,
      projects,
      timeWindow,
      query,
      location,
      aggregate,
      dataset,
      newAlertOrQuery,
      handleMEPAlertDataset,
      environment,
      comparisonDelta,
      triggers,
      thresholdType,
      isQueryValid,
    } = this.props;

    const period = this.getStatsPeriod();
    const renderComparisonStats = Boolean(
      organization.features.includes('change-alerts') && comparisonDelta
    );

    const queryExtras = getMetricDatasetQueryExtras({
      organization,
      location,
      dataset,
      newAlertOrQuery,
    });

    return isSessionAggregate(aggregate) ? (
      <SessionsRequest
        api={api}
        organization={organization}
        project={projects.map(({id}) => Number(id))}
        environment={environment ? [environment] : undefined}
        statsPeriod={period}
        query={query}
        interval={TIME_WINDOW_TO_SESSION_INTERVAL[timeWindow]}
        field={SESSION_AGGREGATE_TO_FIELD[aggregate]}
        groupBy={['session.status']}
      >
        {({loading, errored, reloading, response}) => {
          const {groups, intervals} = response || {};
          const sessionTimeSeries = [
            {
              seriesName:
                AlertWizardAlertNames[
                  getAlertTypeFromAggregateDataset({aggregate, dataset: Dataset.SESSIONS})
                ],
              data: getCrashFreeRateSeries(
                groups,
                intervals,
                SESSION_AGGREGATE_TO_FIELD[aggregate]
              ),
            },
          ];

          return this.renderChart({
            timeseriesData: sessionTimeSeries,
            isLoading: loading,
            isReloading: reloading,
            comparisonData: undefined,
            comparisonMarkLines: undefined,
            minutesThresholdToDisplaySeconds: MINUTES_THRESHOLD_TO_DISPLAY_SECONDS,
            isQueryValid,
            errored,
            orgFeatures: organization.features,
          });
        }}
      </SessionsRequest>
    ) : (
      <EventsRequest
        api={api}
        organization={organization}
        query={query}
        environment={environment ? [environment] : undefined}
        project={projects.map(({id}) => Number(id))}
        interval={`${timeWindow}m`}
        comparisonDelta={comparisonDelta && comparisonDelta * 60}
        period={period}
        yAxis={aggregate}
        includePrevious={false}
        currentSeriesNames={[aggregate]}
        partial={false}
        queryExtras={queryExtras}
        dataLoadedCallback={handleMEPAlertDataset}
      >
        {({
          loading,
          errored,
          errorMessage,
          reloading,
          timeseriesData,
          comparisonTimeseriesData,
        }) => {
          let comparisonMarkLines: LineChartSeries[] = [];
          if (renderComparisonStats && comparisonTimeseriesData) {
            comparisonMarkLines = getComparisonMarkLines(
              timeseriesData,
              comparisonTimeseriesData,
              timeWindow,
              triggers,
              thresholdType
            );
          }

          return this.renderChart({
            timeseriesData: timeseriesData as Series[],
            isLoading: loading,
            isReloading: reloading,
            comparisonData: comparisonTimeseriesData,
            comparisonMarkLines,
            errorMessage,
            isQueryValid,
            errored,
            orgFeatures: organization.features,
          });
        }}
      </EventsRequest>
    );
  }
}

export default withApi(TriggersChart);

const TransparentLoadingMask = styled(LoadingMask)<{visible: boolean}>`
  ${p => !p.visible && 'display: none;'};
  opacity: 0.4;
  z-index: 1;
`;

const ChartPlaceholder = styled(Placeholder)`
  /* Height and margin should add up to graph size (200px) */
  margin: 0 0 ${space(2)};
  height: 184px;
`;

const StyledErrorPanel = styled(ErrorPanel)`
  padding: ${space(2)};
`;

const ChartErrorWrapper = styled('div')`
  margin-top: ${space(2)};
`;
