import { css } from '@emotion/css';
import { useEffect, useMemo, useRef, useCallback, useState, CSSProperties } from 'react';
import * as React from 'react';
import { useTable, Column, TableOptions, Cell } from 'react-table';
import { FixedSizeList } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { Observable } from 'rxjs';

import { Field, GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { TableCellHeight } from '@grafana/schema';
import { useStyles2, useTheme2 } from '@grafana/ui';
import { useTableStyles, TableCell } from '@grafana/ui/internal';
import { useCustomFlexLayout } from 'app/features/browse-dashboards/components/customFlexTableLayout';

import { useSearchKeyboardNavigation } from '../../hooks/useSearchKeyboardSelection';
import { QueryResponse } from '../../service/types';
import { SelectionChecker, SelectionToggle } from '../selection';

import { generateColumns } from './columns';

export type SearchResultsProps = {
  response: QueryResponse;
  width: number;
  height: number;
  selection?: SelectionChecker;
  selectionToggle?: SelectionToggle;
  clearSelection: () => void;
  onTagSelected: (tag: string) => void;
  onDatasourceChange?: (datasource?: string) => void;
  onClickItem?: (event: React.MouseEvent<HTMLElement>) => void;
  keyboardEvents: Observable<React.KeyboardEvent>;
  trackingSource?: string;
};

export type TableColumn = Column & {
  field?: Field;
};

const ROW_HEIGHT = 36; // pixels

export const SearchResultsTable = React.memo(
  ({
    response,
    width,
    height,
    selection,
    selectionToggle,
    clearSelection,
    onTagSelected,
    onDatasourceChange,
    onClickItem,
    keyboardEvents,
    trackingSource,
  }: SearchResultsProps) => {
    const styles = useStyles2(getStyles);
    const columnStyles = useStyles2(getColumnStyles);
    const tableStyles = useTableStyles(useTheme2(), TableCellHeight.Sm);
    const infiniteLoaderRef = useRef<InfiniteLoader>(null);
    const [listEl, setListEl] = useState<FixedSizeList | null>(null);
    const highlightIndex = useSearchKeyboardNavigation(keyboardEvents, 0, response);

    const memoizedData = useMemo(() => {
      if (!response?.view?.dataFrame.fields.length) {
        return [];
      }

      // as we only use this to fake the length of our data set for react-table we need to make sure we always return an array
      // filled with values at each index otherwise we'll end up trying to call accessRow for null|undefined value in
      // https://github.com/tannerlinsley/react-table/blob/7be2fc9d8b5e223fc998af88865ae86a88792fdb/src/hooks/useTable.js#L585
      return Array(response.totalRows).fill(0);
    }, [response]);

    // Scroll to the top and clear loader cache when the query results change
    useEffect(() => {
      if (infiniteLoaderRef.current) {
        infiniteLoaderRef.current.resetloadMoreItemsCache();
      }
      if (listEl) {
        listEl.scrollTo(0);
      }
    }, [memoizedData, listEl]);

    // React-table column definitions
    const memoizedColumns = useMemo(() => {
      return generateColumns(
        response,
        width,
        selection,
        selectionToggle,
        clearSelection,
        columnStyles,
        onTagSelected,
        onDatasourceChange,
        response.view?.length >= response.totalRows
      );
    }, [response, width, columnStyles, selection, selectionToggle, clearSelection, onTagSelected, onDatasourceChange]);

    const options: TableOptions<{}> = useMemo(
      () => ({
        columns: memoizedColumns,
        data: memoizedData,
      }),
      [memoizedColumns, memoizedData]
    );

    const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(options, useCustomFlexLayout);

    const handleLoadMore = useCallback(
      async (startIndex: number, endIndex: number) => {
        await response.loadMoreItems(startIndex, endIndex);

        // After we load more items, select them if the "select all" checkbox
        // is selected
        const isAllSelected = selection?.('*', '*');
        if (!selectionToggle || !selection || !isAllSelected) {
          return;
        }

        for (let index = startIndex; index < response.view.length; index++) {
          const item = response.view.get(index);
          const itemIsSelected = selection(item.kind, item.uid);
          if (!itemIsSelected) {
            selectionToggle(item.kind, item.uid);
          }
        }
      },
      [response, selection, selectionToggle]
    );

    const RenderRow = useCallback(
      ({ index: rowIndex, style }: { index: number; style: CSSProperties }) => {
        const row = rows[rowIndex];
        prepareRow(row);

        const url = response.view.fields.url?.values[rowIndex];
        let className = styles.rowContainer;
        if (rowIndex === highlightIndex.y) {
          className += ' ' + styles.selectedRow;
        }
        const { key, ...rowProps } = row.getRowProps({ style });

        return (
          <div key={key} {...rowProps} className={className}>
            {row.cells.map((cell: Cell, index: number) => {
              const href = onClickItem ? url : undefined;

              let userProps = {
                href,
                onClick: onClickItem,
              };

              if (cell.column.id === 'column-name' && href) {
                const item = response.view.get(rowIndex);
                const itemKind = item.kind;
                const parent = item.location || 'general';
                const parentType = parent === 'general' ? 'general' : 'folder';

                userProps.onClick = (evt: React.MouseEvent<HTMLElement>) => {
                  try {
                    reportInteraction('grafana_browse_dashboards_page_click_list_item', {
                      itemKind: itemKind,
                      parent: parentType,
                      source: trackingSource,
                    });
                  } catch (e) {
                    // ignore analytics errors
                  }
                  if (onClickItem) {
                    onClickItem(evt);
                  }
                };
              }

              return (
                <TableCell
                  key={index}
                  tableStyles={tableStyles}
                  cell={cell}
                  columnIndex={index}
                  columnCount={row.cells.length}
                  userProps={userProps}
                  frame={response.view.dataFrame}
                />
              );
            })}
          </div>
        );
      },
      [rows, prepareRow, highlightIndex, styles, tableStyles, onClickItem, response.view, trackingSource]
    );

    if (!rows.length) {
      return (
        <div className={styles.noData}>
          <Trans i18nKey="search.search-results-table.no-data">No values</Trans>
        </div>
      );
    }

    return (
      <div
        {...getTableProps()}
        aria-label={t('search.search-results-table.aria-label-search-results-table', 'Search results table')}
        role="table"
      >
        {headerGroups.map((headerGroup) => {
          const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps({
            style: { width },
          });

          return (
            <div key={key} {...headerGroupProps} className={styles.headerRow}>
              {headerGroup.headers.map((column) => {
                const { key, ...headerProps } = column.getHeaderProps();
                return (
                  <div key={key} {...headerProps} role="columnheader" className={styles.headerCell}>
                    {column.render('Header')}
                  </div>
                );
              })}
            </div>
          );
        })}

        <div {...getTableBodyProps()}>
          <InfiniteLoader
            ref={infiniteLoaderRef}
            isItemLoaded={response.isItemLoaded}
            itemCount={rows.length}
            loadMoreItems={handleLoadMore}
          >
            {({ onItemsRendered, ref }) => (
              <FixedSizeList
                ref={(innerRef) => {
                  ref(innerRef);
                  setListEl(innerRef);
                }}
                onItemsRendered={onItemsRendered}
                height={height - ROW_HEIGHT}
                itemCount={rows.length}
                itemSize={tableStyles.rowHeight}
                width={width}
                style={{ overflow: 'hidden auto' }}
              >
                {RenderRow}
              </FixedSizeList>
            )}
          </InfiniteLoader>
        </div>
      </div>
    );
  }
);
SearchResultsTable.displayName = 'SearchResultsTable';

const getStyles = (theme: GrafanaTheme2) => {
  const rowHoverBg = theme.colors.action.hover;

  return {
    noData: css({
      display: 'flex',
      flexDirection: 'column',
      alignItems: 'center',
      justifyContent: 'center',
      height: '100%',
    }),
    headerCell: css({
      alignItems: 'center',
      display: 'flex',
      overflo: 'hidden',
      padding: theme.spacing(1),
    }),
    headerRow: css({
      backgroundColor: theme.colors.background.secondary,
      display: 'flex',
      gap: theme.spacing(1),
      height: `${ROW_HEIGHT}px`,
    }),
    selectedRow: css({
      backgroundColor: rowHoverBg,
      boxShadow: `inset 3px 0px ${theme.colors.primary.border}`,
    }),
    rowContainer: css({
      display: 'flex',
      gap: theme.spacing(1),
      height: `${ROW_HEIGHT}px`,
      label: 'row',
      '&:hover': {
        backgroundColor: rowHoverBg,
      },

      "&:not(:hover) div[role='cell']": {
        whiteSpace: 'nowrap',
        overflow: 'hidden',
        textOverflow: 'ellipsis',
      },
    }),
  };
};

// CSS for columns from react table
const getColumnStyles = (theme: GrafanaTheme2) => {
  return {
    cell: css({
      padding: theme.spacing(1),
      overflow: 'hidden', // Required so flex children can do text-overflow: ellipsis
      display: 'flex',
      alignItems: 'center',
    }),
    nameCellStyle: css({
      overflow: 'hidden',
      textOverflow: 'ellipsis',
      userSelect: 'text',
      whiteSpace: 'nowrap',
    }),
    typeCell: css({
      gap: theme.spacing(0.5),
    }),
    typeIcon: css({
      fill: theme.colors.text.secondary,
    }),
    datasourceItem: css({
      span: {
        '&:hover': {
          color: theme.colors.text.link,
        },
      },
    }),
    missingTitleText: css({
      color: theme.colors.text.disabled,
      fontStyle: 'italic',
    }),
    invalidDatasourceItem: css({
      color: theme.colors.error.main,
      textDecoration: 'line-through',
    }),
    locationContainer: css({
      display: 'flex',
      flexWrap: 'nowrap',
      gap: theme.spacing(1),
      // No overflow:hidden here — it would clip the focus ring (box-shadow) from child <a> elements.
      // The parent cell already clips the container width. Each locationItem handles its own truncation.
    }),
    locationItem: css({
      alignItems: 'center',
      color: theme.colors.text.secondary,
      display: 'flex',
      flexWrap: 'nowrap',
      gap: '4px',
      overflow: 'hidden',
    }),
    explainItem: css({
      cursor: 'pointer',
    }),
    tagList: css({
      justifyContent: 'flex-start',
      flexWrap: 'nowrap',
    }),
  };
};
