import {Fragment} from 'react';
import {browserHistory} from 'react-router';
import styled from '@emotion/styled';
import * as Sentry from '@sentry/react';
import type {Location, LocationDescriptorObject} from 'history';

import {openModal} from 'sentry/actionCreators/modal';
import GridEditable, {
  COL_WIDTH_MINIMUM,
  COL_WIDTH_UNDEFINED,
} from 'sentry/components/gridEditable';
import SortLink from 'sentry/components/gridEditable/sortLink';
import Link from 'sentry/components/links/link';
import {Tooltip} from 'sentry/components/tooltip';
import Truncate from 'sentry/components/truncate';
import {IconStack} from 'sentry/icons';
import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types';
import {trackAnalytics} from 'sentry/utils/analytics';
import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
import type {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
import type EventView from 'sentry/utils/discover/eventView';
import {
  isFieldSortable,
  pickRelevantLocationQueryStrings,
} from 'sentry/utils/discover/eventView';
import {
  DURATION_UNITS,
  getFieldRenderer,
  SIZE_UNITS,
} from 'sentry/utils/discover/fieldRenderers';
import type {Column} from 'sentry/utils/discover/fields';
import {
  fieldAlignment,
  getEquationAliasIndex,
  isEquationAlias,
} from 'sentry/utils/discover/fields';
import {DisplayModes, TOP_N} from 'sentry/utils/discover/types';
import {
  eventDetailsRouteWithEventView,
  generateEventSlug,
} from 'sentry/utils/discover/urls';
import ViewReplayLink from 'sentry/utils/discover/viewReplayLink';
import {getShortEventId} from 'sentry/utils/events';
import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes';
import {decodeList} from 'sentry/utils/queryString';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import useProjects from 'sentry/utils/useProjects';
import {useRoutes} from 'sentry/utils/useRoutes';
import {normalizeUrl} from 'sentry/utils/withDomainRequired';
import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
import {generateReplayLink} from 'sentry/views/performance/transactionSummary/utils';

import {
  getExpandedResults,
  getTargetForTransactionSummaryLink,
  pushEventViewToLocation,
} from '../utils';

import {QuickContextHoverWrapper} from './quickContext/quickContextWrapper';
import {ContextType} from './quickContext/utils';
import CellAction, {Actions, updateQuery} from './cellAction';
import ColumnEditModal, {modalCss} from './columnEditModal';
import TableActions from './tableActions';
import TopResultsIndicator from './topResultsIndicator';
import type {TableColumn} from './types';

export type TableViewProps = {
  error: string | null;
  eventView: EventView;
  isFirstPage: boolean;

  isLoading: boolean;
  location: Location;

  measurementKeys: null | string[];
  onChangeShowTags: () => void;
  organization: Organization;
  showTags: boolean;
  tableData: TableData | null | undefined;

  title: string;
  customMeasurements?: CustomMeasurementCollection;
  isHomepage?: boolean;
  spanOperationBreakdownKeys?: string[];
};

/**
 * The `TableView` is marked with leading _ in its method names. It consumes
 * the EventView object given in its props to generate new EventView objects
 * for actions like resizing column.
 *
 * The entire state of the table view (or event view) is co-located within
 * the EventView object. This object is fed from the props.
 *
 * Attempting to modify the state, and therefore, modifying the given EventView
 * object given from its props, will generate new instances of EventView objects.
 *
 * In most cases, the new EventView object differs from the previous EventView
 * object. The new EventView object is pushed to the location object.
 */
function TableView(props: TableViewProps) {
  const {projects} = useProjects();
  const routes = useRoutes();
  const replayLinkGenerator = generateReplayLink(routes);

  /**
   * Updates a column on resizing
   */
  function _resizeColumn(
    columnIndex: number,
    nextColumn: TableColumn<keyof TableDataRow>
  ) {
    const {location, eventView} = props;

    const newWidth = nextColumn.width ? Number(nextColumn.width) : COL_WIDTH_UNDEFINED;
    const nextEventView = eventView.withResizedColumn(columnIndex, newWidth);

    pushEventViewToLocation({
      location,
      nextEventView,
      extraQuery: pickRelevantLocationQueryStrings(location),
    });
  }

  function _renderPrependColumns(
    isHeader: boolean,
    dataRow?: any,
    rowIndex?: number
  ): React.ReactNode[] {
    const {organization, eventView, tableData, location, isHomepage} = props;
    const hasAggregates = eventView.hasAggregateField();
    const hasIdField = eventView.hasIdField();

    if (isHeader) {
      if (hasAggregates) {
        return [
          <PrependHeader key="header-icon">
            <IconStack size="sm" />
          </PrependHeader>,
        ];
      }
      if (!hasIdField) {
        return [
          <PrependHeader key="header-event-id">
            <SortLink
              align="left"
              title={t('event id')}
              direction={undefined}
              canSort={false}
              generateSortLink={() => undefined}
            />
          </PrependHeader>,
        ];
      }
      return [];
    }

    if (hasAggregates) {
      const nextView = getExpandedResults(eventView, {}, dataRow);

      const target = {
        pathname: location.pathname,
        query: nextView.generateQueryStringObject(),
      };

      return [
        <Tooltip key={`eventlink${rowIndex}`} title={t('Open Group')}>
          <Link
            to={target}
            data-test-id="open-group"
            onClick={() => {
              if (nextView.isEqualTo(eventView)) {
                Sentry.captureException(new Error('Failed to drilldown'));
              }
            }}
          >
            <StyledIcon size="sm" />
          </Link>
        </Tooltip>,
      ];
    }
    if (!hasIdField) {
      let value = dataRow.id;

      if (tableData?.meta) {
        const fieldRenderer = getFieldRenderer('id', tableData.meta);
        value = fieldRenderer(dataRow, {organization, location});
      }

      const eventSlug = generateEventSlug(dataRow);

      const target = eventDetailsRouteWithEventView({
        orgSlug: organization.slug,
        eventSlug,
        eventView,
        isHomepage,
      });

      const eventIdLink = (
        <StyledLink data-test-id="view-event" to={target}>
          {value}
        </StyledLink>
      );

      return [
        <QuickContextHoverWrapper
          key={`quickContextEventHover${rowIndex}`}
          dataRow={dataRow}
          contextType={ContextType.EVENT}
          organization={organization}
          projects={projects}
          eventView={eventView}
        >
          {eventIdLink}
        </QuickContextHoverWrapper>,
      ];
    }
    return [];
  }

  function _renderGridHeaderCell(
    column: TableColumn<keyof TableDataRow>
  ): React.ReactNode {
    const {eventView, location, tableData} = props;
    const tableMeta = tableData?.meta;

    const align = fieldAlignment(column.name, column.type, tableMeta);
    const field = {field: column.key as string, width: column.width};
    function generateSortLink(): LocationDescriptorObject | undefined {
      if (!tableMeta) {
        return undefined;
      }

      const nextEventView = eventView.sortOnField(field, tableMeta);
      const queryStringObject = nextEventView.generateQueryStringObject();
      // Need to pull yAxis from location since eventView only stores 1 yAxis field at time
      queryStringObject.yAxis = decodeList(location.query.yAxis);

      return {
        ...location,
        query: queryStringObject,
      };
    }
    const currentSort = eventView.sortForField(field, tableMeta);
    const canSort = isFieldSortable(field, tableMeta);
    let titleText = isEquationAlias(column.name)
      ? eventView.getEquations()[getEquationAliasIndex(column.name)]
      : column.name;

    if (column.name.toLowerCase() === 'replayid') {
      titleText = 'Replay';
    }

    const title = (
      <StyledTooltip title={titleText}>
        <Truncate value={titleText} maxLength={60} expandable={false} />
      </StyledTooltip>
    );

    return (
      <SortLink
        align={align}
        title={title}
        direction={currentSort ? currentSort.kind : undefined}
        canSort={canSort}
        generateSortLink={generateSortLink}
      />
    );
  }

  function _renderGridBodyCell(
    column: TableColumn<keyof TableDataRow>,
    dataRow: TableDataRow,
    rowIndex: number,
    columnIndex: number
  ): React.ReactNode {
    const {isFirstPage, eventView, location, organization, tableData, isHomepage} = props;

    if (!tableData || !tableData.meta) {
      return dataRow[column.key];
    }

    const columnKey = String(column.key);
    const fieldRenderer = getFieldRenderer(columnKey, tableData.meta, false);

    const display = eventView.getDisplayMode();
    const isTopEvents =
      display === DisplayModes.TOP5 || display === DisplayModes.DAILYTOP5;

    const topEvents = eventView.topEvents ? parseInt(eventView.topEvents, 10) : TOP_N;
    const count = Math.min(tableData?.data?.length ?? topEvents, topEvents);

    const unit = tableData.meta.units?.[columnKey];
    let cell = fieldRenderer(dataRow, {organization, location, unit});

    if (columnKey === 'id') {
      const eventSlug = generateEventSlug(dataRow);

      const target = eventDetailsRouteWithEventView({
        orgSlug: organization.slug,
        eventSlug,
        eventView,
        isHomepage,
      });

      const idLink = (
        <StyledLink data-test-id="view-event" to={target}>
          {cell}
        </StyledLink>
      );

      cell = (
        <QuickContextHoverWrapper
          organization={organization}
          dataRow={dataRow}
          contextType={ContextType.EVENT}
          projects={projects}
          eventView={eventView}
        >
          {idLink}
        </QuickContextHoverWrapper>
      );
    } else if (columnKey === 'transaction' && dataRow.transaction) {
      cell = (
        <TransactionLink
          data-test-id="tableView-transaction-link"
          to={getTargetForTransactionSummaryLink(
            dataRow,
            organization,
            projects,
            eventView,
            location
          )}
        >
          {cell}
        </TransactionLink>
      );
    } else if (columnKey === 'trace') {
      const dateSelection = eventView.normalizeDateSelection(location);
      if (dataRow.trace) {
        const target = getTraceDetailsUrl(
          organization,
          String(dataRow.trace),
          dateSelection,
          {},
          dataRow.timestamp,
          dataRow.id
        );

        cell = (
          <Tooltip title={t('View Trace')}>
            <StyledLink data-test-id="view-trace" to={target}>
              {cell}
            </StyledLink>
          </Tooltip>
        );
      }
    } else if (columnKey === 'replayId') {
      if (dataRow.replayId) {
        if (!dataRow['project.name']) {
          return getShortEventId(String(dataRow.replayId));
        }

        const target = replayLinkGenerator(organization, dataRow, undefined);
        cell = (
          <ViewReplayLink replayId={dataRow.replayId} to={target}>
            {cell}
          </ViewReplayLink>
        );
      }
    } else if (columnKey === 'profile.id') {
      const projectSlug = dataRow.project || dataRow['project.name'];
      const profileId = dataRow['profile.id'];

      if (projectSlug && profileId) {
        const target = generateProfileFlamechartRoute({
          orgSlug: organization.slug,
          projectSlug: String(projectSlug),
          profileId: String(profileId),
        });

        cell = (
          <StyledTooltip title={t('View Profile')}>
            <StyledLink
              data-test-id="view-profile"
              to={target}
              onClick={() =>
                trackAnalytics('profiling_views.go_to_flamegraph', {
                  organization,
                  source: 'discover.table',
                })
              }
            >
              {cell}
            </StyledLink>
          </StyledTooltip>
        );
      }
    }

    const topResultsIndicator =
      isFirstPage && isTopEvents && rowIndex < topEvents && columnIndex === 0 ? (
        // Add one if we need to include Other in the series
        <TopResultsIndicator count={count} index={rowIndex} />
      ) : null;

    const fieldName = columnKey;
    const value = dataRow[fieldName];
    if (
      tableData.meta[fieldName] === 'integer' &&
      typeof value === 'number' &&
      value > 999
    ) {
      return (
        <Tooltip
          title={value.toLocaleString()}
          containerDisplayMode="block"
          position="right"
        >
          {topResultsIndicator}
          <CellAction
            column={column}
            dataRow={dataRow}
            handleCellAction={handleCellAction(dataRow, column)}
          >
            {cell}
          </CellAction>
        </Tooltip>
      );
    }

    return (
      <Fragment>
        {topResultsIndicator}
        <CellAction
          column={column}
          dataRow={dataRow}
          handleCellAction={handleCellAction(dataRow, column)}
        >
          {cell}
        </CellAction>
      </Fragment>
    );
  }

  function handleEditColumns() {
    const {
      organization,
      eventView,
      measurementKeys,
      spanOperationBreakdownKeys,
      customMeasurements,
    } = props;

    openModal(
      modalProps => (
        <ColumnEditModal
          {...modalProps}
          organization={organization}
          measurementKeys={measurementKeys}
          spanOperationBreakdownKeys={spanOperationBreakdownKeys}
          columns={eventView.getColumns().map(col => col.column)}
          onApply={handleUpdateColumns}
          customMeasurements={customMeasurements}
        />
      ),
      {modalCss, closeEvents: 'escape-key'}
    );
  }

  function handleCellAction(
    dataRow: TableDataRow,
    column: TableColumn<keyof TableDataRow>
  ) {
    return (action: Actions, value: React.ReactText) => {
      const {eventView, organization, location, tableData, isHomepage} = props;

      const query = new MutableSearch(eventView.query);

      let nextView = eventView.clone();
      trackAnalytics('discover_v2.results.cellaction', {
        organization,
        action,
      });

      switch (action) {
        case Actions.RELEASE: {
          const maybeProject = projects.find(project => {
            return project.slug === dataRow.project;
          });

          browserHistory.push(
            normalizeUrl({
              pathname: `/organizations/${
                organization.slug
              }/releases/${encodeURIComponent(value)}/`,
              query: {
                ...nextView.getPageFiltersQuery(),

                project: maybeProject ? maybeProject.id : undefined,
              },
            })
          );

          return;
        }
        case Actions.DRILLDOWN: {
          // count_unique(column) drilldown
          trackAnalytics('discover_v2.results.drilldown', {
            organization,
          });

          // Drilldown into each distinct value and get a count() for each value.
          nextView = getExpandedResults(nextView, {}, dataRow).withNewColumn({
            kind: 'function',
            function: ['count', '', undefined, undefined],
          });

          browserHistory.push(
            normalizeUrl(nextView.getResultsViewUrlTarget(organization.slug, isHomepage))
          );

          return;
        }
        default: {
          // Some custom perf metrics have units.
          // These custom perf metrics need to be adjusted to the correct value.
          let cellValue = value;
          const unit = tableData?.meta?.units?.[column.name];
          if (typeof cellValue === 'number' && unit) {
            if (Object.keys(SIZE_UNITS).includes(unit)) {
              cellValue *= SIZE_UNITS[unit];
            } else if (Object.keys(DURATION_UNITS).includes(unit)) {
              cellValue *= DURATION_UNITS[unit];
            }
          }
          updateQuery(query, action, column, cellValue);
        }
      }
      nextView.query = query.formatString();

      const target = nextView.getResultsViewUrlTarget(organization.slug, isHomepage);
      // Get yAxis from location
      target.query.yAxis = decodeList(location.query.yAxis);
      browserHistory.push(normalizeUrl(target));
    };
  }

  function handleUpdateColumns(columns: Column[]): void {
    const {organization, eventView, location, isHomepage} = props;

    // metrics
    trackAnalytics('discover_v2.update_columns', {
      organization,
    });

    const nextView = eventView.withColumns(columns);
    const resultsViewUrlTarget = nextView.getResultsViewUrlTarget(
      organization.slug,
      isHomepage
    );
    // Need to pull yAxis from location since eventView only stores 1 yAxis field at time
    const previousYAxis = decodeList(location.query.yAxis);
    resultsViewUrlTarget.query.yAxis = previousYAxis.filter(yAxis =>
      nextView.getYAxisOptions().find(({value}) => value === yAxis)
    );
    browserHistory.push(normalizeUrl(resultsViewUrlTarget));
  }

  function renderHeaderButtons() {
    const {
      organization,
      title,
      eventView,
      isLoading,
      error,
      tableData,
      location,
      onChangeShowTags,
      showTags,
    } = props;

    return (
      <TableActions
        title={title}
        isLoading={isLoading}
        error={error}
        organization={organization}
        eventView={eventView}
        onEdit={handleEditColumns}
        tableData={tableData}
        location={location}
        onChangeShowTags={onChangeShowTags}
        showTags={showTags}
        supportsInvestigationRule
      />
    );
  }

  const {error, eventView, isLoading, location, tableData} = props;

  const columnOrder = eventView.getColumns();
  const columnSortBy = eventView.getSorts();

  const prependColumnWidths = eventView.hasAggregateField()
    ? ['40px']
    : eventView.hasIdField()
      ? []
      : [`minmax(${COL_WIDTH_MINIMUM}px, max-content)`];

  return (
    <GridEditable
      isLoading={isLoading}
      error={error}
      data={tableData ? tableData.data : []}
      columnOrder={columnOrder}
      columnSortBy={columnSortBy}
      title={t('Results')}
      grid={{
        renderHeadCell: _renderGridHeaderCell as any,
        renderBodyCell: _renderGridBodyCell as any,
        onResizeColumn: _resizeColumn as any,
        renderPrependColumns: _renderPrependColumns as any,
        prependColumnWidths,
      }}
      headerButtons={renderHeaderButtons}
      location={location}
    />
  );
}

const PrependHeader = styled('span')`
  color: ${p => p.theme.subText};
`;

const StyledTooltip = styled(Tooltip)`
  display: initial;
  max-width: max-content;
`;

export const StyledLink = styled(Link)`
  & div {
    display: inline;
  }
`;

export const TransactionLink = styled(Link)`
  ${p => p.theme.overflowEllipsis}
`;

const StyledIcon = styled(IconStack)`
  vertical-align: middle;
`;

export default TableView;
