import { css, cx } from '@emotion/css';
import { useBooleanFlagValue } from '@openfeature/react-sdk';
import { groupBy } from 'lodash';
import * as React from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { usePrevious, useUnmount } from 'react-use';

import {
  type AbsoluteTimeRange,
  compareArrayValues,
  CoreApp,
  type DataFrame,
  DataHoverClearEvent,
  DataHoverEvent,
  type DataQueryResponse,
  type EventBus,
  type ExploreLogsPanelState,
  type ExplorePanelsState,
  type GrafanaTheme2,
  LoadingState,
  type LogLevel,
  type LogRowContextOptions,
  type LogRowModel,
  LogsDedupStrategy,
  type LogsMetaItem,
  LogsSortOrder,
  type PanelData,
  rangeUtil,
  type RawTimeRange,
  serializeStateToUrlParam,
  shallowCompare,
  type SplitOpen,
  store,
  type TimeRange,
  type TimeZone,
  toUtc,
  urlUtil,
} from '@grafana/data';
import { t, Trans } from '@grafana/i18n';
import { config, reportInteraction } from '@grafana/runtime';
import { type DataQuery, DataTopic, type TableSortByFieldState } from '@grafana/schema';
import {
  Button,
  PanelChrome,
  type PopoverContent,
  RadioButtonGroup,
  SeriesVisibilityChangeMode,
  type Themeable2,
  withTheme2,
} from '@grafana/ui';
import { createAndCopyShortLink, getLogsPermalinkRange } from 'app/core/utils/shortLinks';
import { ControlledLogRows } from 'app/features/logs/components/ControlledLogRows';
import { LogRowContextModal } from 'app/features/logs/components/log-context/LogRowContextModal';
import { LogLineContext } from 'app/features/logs/components/panel/LogLineContext';
import { LogList, type LogListOptions } from 'app/features/logs/components/panel/LogList';
import { isDedupStrategy, isLogsSortOrder } from 'app/features/logs/components/panel/LogListContext';
import { dedupLogRows, LogLevelColor } from 'app/features/logs/logsModel';
import { getLogLevelFromKey, getLogLevelInfo, isMissingTimeField } from 'app/features/logs/utils';
import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen';
import { isLokiQuery } from 'app/plugins/datasource/loki/queryUtils';
import { type GetFieldLinksFn } from 'app/plugins/panel/logs/types';
import { type Options } from 'app/plugins/panel/logstable/options/types';
import { type BuildLinkToLogLine } from 'app/plugins/panel/logstable/types';
import { getState } from 'app/store/store';
import { type ExploreItemState } from 'app/types/explore';
import { useDispatch } from 'app/types/store';

import { getDefaultFieldSelectorWidth } from '../../logs/components/fieldSelector/FieldSelector';
import {
  contentOutlineTrackPinAdded,
  contentOutlineTrackPinClicked,
  contentOutlineTrackPinLimitReached,
  contentOutlineTrackPinRemoved,
  contentOutlineTrackUnpinClicked,
} from '../ContentOutline/ContentOutlineAnalyticEvents';
import { useContentOutlineContext } from '../ContentOutline/ContentOutlineContext';
import { getUrlStateFromPaneState } from '../hooks/useStateSync/external.utils';
import { changePanelState } from '../state/explorePane';
import { changeQueries, runQueries } from '../state/query';

import { ExploreLogsTable } from './ExploreLogsTable';
import { LogsFeedback } from './LogsFeedback';
import { LogsMetaRow } from './LogsMetaRow';
import { getLogsTableHeight } from './LogsTableWrap';
import { LogsVolumePanelList } from './LogsVolumePanelList';
import { type LogsVisualisationType } from './constants';
import { LOGS_TABLE_SETTING_KEYS, SETTING_KEY_ROOT, SETTINGS_KEYS, visualisationTypeKey } from './utils/logs';
import { getDefaultDisplayedFieldsFromExploreState } from './utils/table/columnsMigration';
import { getDefaultTableSortBy } from './utils/table/logsTable';
import { getExploreBaseUrl } from './utils/url';

interface Props extends Themeable2 {
  width: number;
  splitOpen: SplitOpen;
  logRows: LogRowModel[];
  logsMeta?: LogsMetaItem[];
  logsSeries?: DataFrame[];
  logsQueries?: DataQuery[];
  visibleRange?: AbsoluteTimeRange;
  theme: GrafanaTheme2;
  loading: boolean;
  loadingState: LoadingState;
  absoluteRange: AbsoluteTimeRange;
  timeZone: TimeZone;
  scanning?: boolean;
  scanRange?: RawTimeRange;
  exploreId: string;
  datasourceType?: string;
  logsVolumeEnabled: boolean;
  logsVolumeData: DataQueryResponse | undefined;
  onSetLogsVolumeEnabled: (enabled: boolean) => void;
  loadLogsVolumeData: () => void;
  showContextToggle?: (row: LogRowModel) => boolean;
  onChangeTime: (range: AbsoluteTimeRange) => void;
  onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void;
  onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void;
  onStartScanning?: () => void;
  onStopScanning?: () => void;
  getRowContext?: (row: LogRowModel, origRow: LogRowModel, options: LogRowContextOptions) => Promise<DataQueryResponse>;
  getRowContextQuery?: (
    row: LogRowModel,
    options?: LogRowContextOptions,
    cacheFilters?: boolean
  ) => Promise<DataQuery | null>;
  getLogRowContextUi?: (row: LogRowModel, runContextQuery?: () => void) => React.ReactNode;
  getFieldLinks: GetFieldLinksFn;
  eventBus: EventBus;
  panelState?: ExplorePanelsState;
  isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
  logsFrames: DataFrame[] | undefined;
  range: TimeRange;
  onClickFilterString?: (value: string, refId?: string) => void;
  onClickFilterOutString?: (value: string, refId?: string) => void;
  loadMoreLogs?(range: AbsoluteTimeRange): void;
  onPinLineCallback?: () => void;
}

const getDefaultVisualisationType = (): LogsVisualisationType => {
  const visualisationType = store.get(visualisationTypeKey);
  if (visualisationType === 'table') {
    return 'table';
  }
  if (visualisationType === 'logs') {
    return 'logs';
  }
  return 'logs';
};

const PINNED_LOGS_LIMIT = 10;
const PINNED_LOGS_TITLE = 'Pinned log';
const PINNED_LOGS_MESSAGE = 'Pin to content outline';
const PINNED_LOGS_PANELID = 'Logs';

const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
  const {
    width,
    splitOpen,
    logRows,
    logsMeta,
    logsVolumeEnabled,
    logsVolumeData,
    loadLogsVolumeData,
    loading = false,
    onClickFilterLabel,
    onClickFilterOutLabel,
    timeZone,
    scanning,
    scanRange,
    showContextToggle,
    absoluteRange,
    onChangeTime,
    getFieldLinks,
    theme,
    logsQueries,
    exploreId,
    getRowContext,
    getLogRowContextUi,
    getRowContextQuery,
    loadMoreLogs,
    panelState,
    eventBus,
    onPinLineCallback,
  } = props;
  const showLabels = store.getBool(SETTINGS_KEYS.showLabels, false);
  const showTime = store.getBool(SETTINGS_KEYS.showTime, true);
  const wrapLogMessage = store.getBool(SETTINGS_KEYS.wrapLogMessage, true);
  const prettifyLogMessage = store.getBool(SETTINGS_KEYS.prettifyLogMessage, false);

  const [dedupStrategy, setDedupStrategy] = useState<LogsDedupStrategy>(LogsDedupStrategy.none);
  const [logsSortOrder, setLogsSortOrder] = useState<LogsSortOrder>(
    panelState?.logs?.sortOrder ?? store.get(SETTINGS_KEYS.logsSortOrder) ?? LogsSortOrder.Descending
  );
  const tableFrameIndex: number = useMemo(() => {
    const frameIndex = props.logsFrames?.findIndex((f) => f.refId === props.panelState?.logs?.refId);
    if (frameIndex !== undefined && frameIndex !== -1) {
      return frameIndex;
    }
    return 0;
  }, [props.logsFrames, props.panelState?.logs?.refId]);

  const tableSortByDefaultStringFromStorage: string = store.get(LOGS_TABLE_SETTING_KEYS.sortBy);
  const [tableSortBy, setTableSortBy] = useState<TableSortByFieldState[]>(
    getDefaultTableSortBy(tableSortByDefaultStringFromStorage, props?.logsFrames?.[tableFrameIndex], logsSortOrder)
  );

  const fieldSelectorWidthFromStorage = store.get(LOGS_TABLE_SETTING_KEYS.fieldSelectorWidth);
  const [fieldSelectorWidth, setFieldSelectorWidth] = useState(
    fieldSelectorWidthFromStorage ? parseInt(fieldSelectorWidthFromStorage, 10) : getDefaultFieldSelectorWidth()
  );
  const [defaultDisplayedFields, setDefaultDisplayedFields] = useState<string[]>([]);
  const [contextOpen, setContextOpen] = useState<boolean>(false);
  const [contextRow, setContextRow] = useState<LogRowModel | undefined>(undefined);
  const [pinLineButtonTooltipTitle, setPinLineButtonTooltipTitle] = useState<PopoverContent>(PINNED_LOGS_MESSAGE);
  const [visualisationType, setVisualisationType] = useState<LogsVisualisationType>(
    panelState?.logs?.visualisationType ?? getDefaultVisualisationType()
  );
  const logsContainerRef = useRef<HTMLDivElement | null>(null);
  const [logListContainerElement, setLogListContainerElement] = useState<HTMLDivElement | null>(null);
  const dispatch = useDispatch();
  const previousLoading = usePrevious(loading);

  const logsVolumeEventBus = eventBus.newScopedBus('logsvolume', { onlyLocal: false });
  const { register, unregister, outlineItems, updateItem } = useContentOutlineContext() ?? {};
  const toggleLegendRef = useRef<(name: string | undefined, mode: SeriesVisibilityChangeMode) => void>(() => {});
  const [filterLevels, setFilterLevels] = useState<LogLevel[] | undefined>(undefined);
  const newLogsPanelEnabled = useBooleanFlagValue('newLogsPanel', true);
  const newLogContextEnabled = useBooleanFlagValue('newLogContext', false);

  const tableHeight = getLogsTableHeight();
  const setWrapperLineWrapStyles = wrapLogMessage || visualisationType === 'table';
  const styles = getStyles(theme, setWrapperLineWrapStyles, tableHeight);
  const hasData = logRows && logRows.length > 0;
  const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';

  const updatePanelState = useCallback(
    (logsPanelState: Partial<ExploreLogsPanelState>) => {
      const state: ExploreItemState | undefined = getState().explore.panes[exploreId];
      if (state?.panelsState) {
        dispatch(
          changePanelState(exploreId, 'logs', {
            ...state.panelsState.logs,
            columns: logsPanelState.columns ?? panelState?.logs?.columns,
            visualisationType: logsPanelState.visualisationType ?? visualisationType,
            labelFieldName: logsPanelState.labelFieldName,
            refId: logsPanelState.refId ?? panelState?.logs?.refId,
            displayedFields: logsPanelState.displayedFields ?? panelState?.logs?.displayedFields,
            tableSortBy: logsPanelState.tableSortBy ?? panelState?.logs?.tableSortBy,
            tableSortDir: logsPanelState.tableSortDir ?? panelState?.logs?.tableSortDir,
          })
        );
      }
    },
    [
      dispatch,
      exploreId,
      panelState?.logs?.columns,
      panelState?.logs?.displayedFields,
      panelState?.logs?.refId,
      panelState?.logs?.tableSortBy,
      panelState?.logs?.tableSortDir,
      visualisationType,
    ]
  );

  const [displayedFields, setDisplayedFields] = useState<string[]>(() =>
    getDefaultDisplayedFieldsFromExploreState(
      panelState?.logs,
      updatePanelState,
      config.featureToggles.logsTablePanelNG ?? false // Use the old feature toggles on initial load, newer useBooleanFlagValue hooks can use default of first render
    )
  );

  // Get pinned log lines
  const logsParent = outlineItems?.find((item) => item.panelId === PINNED_LOGS_PANELID && item.level === 'root');
  const pinnedLogs = useMemo(
    () =>
      logsParent?.children
        ?.filter((outlines) => outlines.title === PINNED_LOGS_TITLE)
        .map((pinnedLogs) => pinnedLogs.id),
    [logsParent?.children]
  );

  const getPinnedLogsCount = useCallback(() => {
    const logsParent = outlineItems?.find((item) => item.panelId === PINNED_LOGS_PANELID && item.level === 'root');
    return logsParent?.children?.filter((child) => child.title === PINNED_LOGS_TITLE).length ?? 0;
  }, [outlineItems]);

  const handleSetTableSortBy = useCallback((sortBy: TableSortByFieldState[]) => {
    store.set(LOGS_TABLE_SETTING_KEYS.sortBy, JSON.stringify(sortBy));
    setTableSortBy(sortBy);
  }, []);

  useEffect(() => {
    if (getPinnedLogsCount() === PINNED_LOGS_LIMIT) {
      setPinLineButtonTooltipTitle(
        <span style={{ display: 'flex', textAlign: 'center' }}>
          ❗️
          <Trans i18nKey="explore.logs.maximum-pinned-logs">
            Maximum of {{ PINNED_LOGS_LIMIT }} pinned logs reached. Unpin a log to add another.
          </Trans>
        </span>
      );
    } else {
      setPinLineButtonTooltipTitle(PINNED_LOGS_MESSAGE);
    }
  }, [outlineItems, getPinnedLogsCount]);

  useEffect(() => {
    if (loading && !previousLoading && panelState?.logs?.id) {
      // loading stopped, so we need to remove any permalinked log lines
      delete panelState.logs.id;

      dispatch(
        changePanelState(exploreId, 'logs', {
          ...panelState,
        })
      );
    }
  }, [dispatch, exploreId, loading, panelState, previousLoading]);

  useEffect(() => {
    // Initialize URL sort order
    if (!panelState?.logs?.sortOrder) {
      dispatch(
        changePanelState(exploreId, 'logs', {
          ...panelState,
          sortOrder: logsSortOrder,
        })
      );
    }
  }, [dispatch, exploreId, logsSortOrder, panelState]);

  useEffect(() => {
    const visualisationType = panelState?.logs?.visualisationType ?? getDefaultVisualisationType();
    setVisualisationType(visualisationType);

    store.set(visualisationTypeKey, visualisationType);
  }, [panelState?.logs?.visualisationType]);

  useUnmount(() => {
    // If we're unmounting logs (e.g. switching to another datasource), we need to remove the logs specific panel state, otherwise it will persist in the explore url
    dispatch(
      changePanelState(exploreId, 'logs', {
        ...panelState?.logs,
        columns: undefined,
        visualisationType: visualisationType,
        labelFieldName: undefined,
        refId: undefined,
        displayedFields: undefined,
        sortOrder: undefined,
        tableSortBy: undefined,
        tableSortDir: undefined,
      })
    );
  });

  useEffect(() => {
    if (!shallowCompare(displayedFields, panelState?.logs?.displayedFields ?? [])) {
      updatePanelState({
        ...panelState?.logs,
        displayedFields,
      });
    }
  }, [displayedFields, panelState?.logs, updatePanelState]);

  // actions
  const onLogRowHover = useCallback(
    (row?: LogRowModel) => {
      if (!row) {
        props.eventBus.publish(new DataHoverClearEvent());
      } else {
        props.eventBus.publish(
          new DataHoverEvent({
            point: {
              time: row.timeEpochMs,
            },
          })
        );
      }
    },
    [props.eventBus]
  );

  const sortOrderChanged = useCallback(
    (newSortOrder: LogsSortOrder) => {
      if (!logsQueries) {
        return;
      }
      let hasLokiQueries = false;
      const newQueries = logsQueries.map((query) => {
        if (query.datasource?.type !== 'loki' || !isLokiQuery(query)) {
          return query;
        }
        if (query.direction === LokiQueryDirection.Scan) {
          // Don't override Scan. When the direction is Scan it means that the user specifically assigned this direction to the query.
          return query;
        }
        hasLokiQueries = true;
        const newDirection =
          newSortOrder === LogsSortOrder.Ascending ? LokiQueryDirection.Forward : LokiQueryDirection.Backward;
        if (newDirection !== query.direction) {
          query.direction = newDirection;
        }
        return query;
      });

      if (hasLokiQueries) {
        dispatch(changeQueries({ exploreId, queries: newQueries }));
        dispatch(runQueries({ exploreId }));
      }
      dispatch(
        changePanelState(exploreId, 'logs', {
          ...panelState?.logs,
          sortOrder: newSortOrder,
        })
      );
    },
    [dispatch, exploreId, logsQueries, panelState?.logs]
  );

  const onChangeLogsSortOrder = useCallback(
    (newSortOrder: LogsSortOrder) => {
      store.set(SETTINGS_KEYS.logsSortOrder, newSortOrder);
      sortOrderChanged(newSortOrder);
      setLogsSortOrder(newSortOrder);
    },
    [sortOrderChanged]
  );

  const onChangeVisualisation = useCallback(
    (visualisation: LogsVisualisationType) => {
      setVisualisationType(visualisation);
      const payload = {
        ...panelState?.logs,
        visualisationType: visualisation,
      };
      updatePanelState(payload);

      reportInteraction('grafana_explore_logs_visualisation_changed', {
        newVisualizationType: visualisation,
        datasourceType: props.datasourceType ?? 'unknown',
      });
    },
    [panelState?.logs, props.datasourceType, updatePanelState]
  );

  const onToggleLogsVolumeCollapse = useCallback(
    (collapsed: boolean) => {
      props.onSetLogsVolumeEnabled(!collapsed);
      reportInteraction('grafana_explore_logs_histogram_toggle_clicked', {
        datasourceType: props.datasourceType,
        type: !collapsed ? 'open' : 'close',
      });
    },
    [props]
  );

  const onClickScan = useCallback(
    (event: React.SyntheticEvent) => {
      event.preventDefault();
      if (props.onStartScanning) {
        props.onStartScanning();
        reportInteraction('grafana_explore_logs_scanning_button_clicked', {
          type: 'start',
          datasourceType: props.datasourceType,
        });
      }
    },
    [props]
  );

  const onClickStopScan = useCallback(
    (event: React.SyntheticEvent) => {
      event.preventDefault();
      if (props.onStopScanning) {
        props.onStopScanning();
      }
    },
    [props]
  );

  const showField = useCallback(
    (key: string) => {
      const index = displayedFields.indexOf(key);

      if (index === -1) {
        const updatedDisplayedFields = displayedFields.concat(key);
        setDisplayedFields(updatedDisplayedFields);
      }
    },
    [displayedFields]
  );

  const hideField = useCallback(
    (key: string) => {
      const index = displayedFields.indexOf(key);
      if (index > -1) {
        const updatedDisplayedFields = displayedFields.filter((k) => key !== k);
        setDisplayedFields(updatedDisplayedFields);
      }
    },
    [displayedFields]
  );

  const clearDisplayedFields = useCallback(() => {
    setDisplayedFields([]);
  }, []);

  const onCloseCallbackRef = useRef<() => void>(() => {});

  let onCloseContext = useCallback(() => {
    setContextOpen(false);
    setContextRow(undefined);
    if (!newLogContextEnabled) {
      reportInteraction('grafana_explore_logs_log_context_closed', {
        datasourceType: contextRow?.datasourceType,
        logRowUid: contextRow?.uid,
      });
    }
    onCloseCallbackRef?.current();
  }, [contextRow?.datasourceType, contextRow?.uid, newLogContextEnabled, onCloseCallbackRef]);

  const onOpenContext = useCallback(
    (row: LogRowModel, onClose: () => void) => {
      // we are setting the `contextOpen` open state and passing it down to the `LogRow` in order to highlight the row when a LogContext is open
      setContextOpen(true);
      setContextRow(row);
      if (!newLogContextEnabled) {
        reportInteraction('grafana_explore_logs_log_context_opened', {
          datasourceType: row.datasourceType,
          logRowUid: row.uid,
        });
      }
      onCloseCallbackRef.current = onClose;
    },
    [newLogContextEnabled]
  );

  const onPermalinkClick = useCallback(
    async (row: LogRowModel) => {
      // this is an extra check, to be sure that we are not
      // creating permalinks for logs without an id-field.
      // normally it should never happen, because we do not
      // display the permalink button in such cases.
      if (row.rowId === undefined) {
        return;
      }

      // get explore state, add log-row-id and make timerange absolute
      const urlState = getUrlStateFromPaneState(getState().explore.panes[exploreId]!);
      urlState.panelsState = {
        ...panelState,
        logs: {
          id: row.uid,
          visualisationType: visualisationType ?? getDefaultVisualisationType(),
          displayedFields,
          sortOrder: logsSortOrder,
        },
      };
      urlState.range = getLogsPermalinkRange(row, logRows, absoluteRange);

      // append changed urlState to baseUrl
      const serializedState = serializeStateToUrlParam(urlState);
      const baseUrl = getExploreBaseUrl();
      const url = urlUtil.renderUrl(`${baseUrl}/explore`, { left: serializedState });
      await createAndCopyShortLink(url);

      reportInteraction('grafana_explore_logs_permalink_clicked', {
        datasourceType: row.datasourceType ?? 'unknown',
        logRowUid: row.uid,
        logRowLevel: row.logLevel,
      });
    },
    [absoluteRange, displayedFields, exploreId, logRows, logsSortOrder, panelState, visualisationType]
  );

  const onTablePermalinkClick: BuildLinkToLogLine = useCallback(
    (logId: string) => {
      // this is an extra check, to be sure that we are not
      // creating permalinks for logs without an id-field.
      // normally it should never happen, because we do not
      // display the permalink button in such cases.
      if (logId === undefined) {
        return null;
      }

      // get explore state, add log-row-id and make timerange absolute
      const urlState = getUrlStateFromPaneState(getState().explore.panes[exploreId]!);
      urlState.panelsState = {
        ...panelState,
        logs: {
          id: logId,
          visualisationType: visualisationType ?? getDefaultVisualisationType(),
          displayedFields,
          sortOrder: logsSortOrder,
        },
      };

      urlState.range = {
        from: toUtc(props.range.from).valueOf().toString(),
        to: toUtc(props.range.to).valueOf().toString(),
      };

      // append changed urlState to baseUrl
      const serializedState = serializeStateToUrlParam(urlState);
      const baseUrl = getExploreBaseUrl();
      const url = urlUtil.renderUrl(`${baseUrl}/explore`, { left: serializedState });

      createAndCopyShortLink(url);

      reportInteraction('grafana_explore_logs_table_permalink_clicked', {
        logRowUid: logId,
      });

      return url;
    },
    [displayedFields, exploreId, logsSortOrder, panelState, props.range.from, props.range.to, visualisationType]
  );

  const onPinToContentOutlineClick = useCallback(
    (row: LogRowModel, allowUnPin = true) => {
      if (getPinnedLogsCount() === PINNED_LOGS_LIMIT && !allowUnPin) {
        contentOutlineTrackPinLimitReached();
        return;
      }

      // find the Logs parent item
      const logsParent = outlineItems?.find((item) => item.panelId === PINNED_LOGS_PANELID && item.level === 'root');

      //update the parent's expanded state
      if (logsParent && updateItem) {
        updateItem(logsParent.id, { expanded: true });
      }

      const alreadyPinned = pinnedLogs?.find((pin) => pin === row.rowId);
      if (alreadyPinned && row.rowId && allowUnPin) {
        unregister?.(row.rowId);
        contentOutlineTrackPinRemoved();
      } else if (getPinnedLogsCount() !== PINNED_LOGS_LIMIT && !alreadyPinned) {
        register?.({
          id: row.rowId,
          icon: 'gf-logs',
          title: PINNED_LOGS_TITLE,
          panelId: PINNED_LOGS_PANELID,
          level: 'child',
          ref: null,
          color: LogLevelColor[row.logLevel],
          childOnTop: true,
          onClick: () => {
            onOpenContext(row, () => {});
            contentOutlineTrackPinClicked();
          },
          onRemove: (id: string) => {
            unregister?.(id);
            contentOutlineTrackUnpinClicked();
          },
        });
        contentOutlineTrackPinAdded();
      }

      onPinLineCallback?.();
    },
    [getPinnedLogsCount, onOpenContext, onPinLineCallback, outlineItems, pinnedLogs, register, unregister, updateItem]
  );

  const { dedupedRows, dedupCount } = useMemo(() => dedupRows(logRows, dedupStrategy), [dedupStrategy, logRows]);
  const infiniteScrollAvailable = useMemo(
    () => !logsQueries?.some((query) => 'direction' in query && query.direction === LokiQueryDirection.Scan),
    [logsQueries]
  );

  const visibilityChangedRef = useRef(true);
  const onLogOptionsChange = useCallback(
    (option: LogListOptions, value: string | string[] | boolean) => {
      if (option === 'sortOrder' && isLogsSortOrder(value)) {
        sortOrderChanged(value);
        setLogsSortOrder(value);
      } else if (option === 'dedupStrategy' && isDedupStrategy(value)) {
        setDedupStrategy(value);
      } else if (option === 'filterLevels' && Array.isArray(value) && logsVolumeEnabled) {
        const logVolData =
          logsVolumeData?.data.filter((frame: DataFrame) => frame.meta?.dataTopic !== DataTopic.Annotations) ?? [];
        const grouped = groupBy(logVolData, 'meta.custom.datasourceName');
        const numberOfLogVolumes = Object.keys(grouped).length;

        // Not supported
        if (numberOfLogVolumes > 1) {
          return;
        }

        const logsVolumeLevels = [
          ...new Set(
            logVolData.map((dataFrame) => {
              const { level } = getLogLevelInfo(dataFrame, logVolData);
              return getLogLevelFromKey(level);
            })
          ),
        ];

        setFilterLevels((currentFilterLevels: LogLevel[] | undefined) => {
          const newLevels = value.map((level) => getLogLevelFromKey(level));
          const newLogsVolumeLevels = newLevels.filter((level) => logsVolumeLevels.includes(level));
          const prevLogsVolumeLevels = currentFilterLevels?.filter((level) => logsVolumeLevels.includes(level)) ?? [];

          if (!newLogsVolumeLevels.length) {
            toggleLegendRef.current?.(undefined, SeriesVisibilityChangeMode.ToggleSelection);
            return undefined;
          }

          const addedLevels = newLogsVolumeLevels.filter((newLevel) => !prevLogsVolumeLevels.includes(newLevel));
          const removedLevels = prevLogsVolumeLevels.filter((prevLevel) => !newLogsVolumeLevels.includes(prevLevel));

          addedLevels.forEach((level) => {
            visibilityChangedRef.current = true;
            toggleLegendRef.current?.(
              level,
              !prevLogsVolumeLevels.length
                ? SeriesVisibilityChangeMode.ToggleSelection
                : SeriesVisibilityChangeMode.AppendToSelection
            );
          });
          removedLevels.forEach((level) => {
            visibilityChangedRef.current = true;
            toggleLegendRef.current?.(level, SeriesVisibilityChangeMode.AppendToSelection);
          });

          return newLevels;
        });
      } else if (option === 'defaultDisplayedFields' && Array.isArray(value)) {
        setDefaultDisplayedFields(value);
      }
    },
    [logsVolumeData?.data, logsVolumeEnabled, sortOrderChanged]
  );

  const onTableOptionsChange = useCallback(
    (options: Options) => {
      if (options.displayedFields && !shallowCompare(options.displayedFields, displayedFields)) {
        setDisplayedFields(options.displayedFields);
      }
      if (options.sortOrder && options.sortOrder !== logsSortOrder) {
        onChangeLogsSortOrder(options.sortOrder);
      }
      if (options.fieldSelectorWidth !== undefined && options.fieldSelectorWidth !== fieldSelectorWidth) {
        store.set(LOGS_TABLE_SETTING_KEYS.fieldSelectorWidth, options.fieldSelectorWidth);
        setFieldSelectorWidth(options.fieldSelectorWidth);
      }
      if (
        options.sortBy &&
        !compareArrayValues(
          options?.sortBy ?? [],
          tableSortBy ?? [],
          (a, b) => a.displayName === b.displayName && a.desc === b.desc
        )
      ) {
        handleSetTableSortBy(options.sortBy);
      }

      if (options.frameIndex !== tableFrameIndex) {
        const refId = props?.logsFrames?.[options.frameIndex]?.refId;
        if (refId) {
          updatePanelState({ refId });
        }
      }
    },
    [
      displayedFields,
      fieldSelectorWidth,
      handleSetTableSortBy,
      logsSortOrder,
      onChangeLogsSortOrder,
      props?.logsFrames,
      tableFrameIndex,
      tableSortBy,
      updatePanelState,
    ]
  );

  const tableOptions = useMemo(
    () => ({
      sortOrder: logsSortOrder,
      sortBy: tableSortBy,
      displayedFields: displayedFields,
      permalinkedLogId: props.panelState?.logs?.id,
      frameIndex: tableFrameIndex,
      fieldSelectorWidth: fieldSelectorWidth,
    }),
    [displayedFields, fieldSelectorWidth, logsSortOrder, props.panelState?.logs?.id, tableFrameIndex, tableSortBy]
  );

  const onDisplayedSeriesChanged = useCallback((levels: string[]) => {
    if (visibilityChangedRef.current) {
      visibilityChangedRef.current = false;
      return;
    }
    setFilterLevels(levels.map((level) => getLogLevelFromKey(level)));
  }, []);

  const enableNewLogsTable = config.featureToggles.logsTablePanelNG;
  const panelData: PanelData = {
    state: loading ? LoadingState.Loading : LoadingState.Done,
    series: props.logsFrames ?? [],
    timeRange: props.range,
  };

  return (
    <>
      {(!newLogsPanelEnabled || !newLogContextEnabled) && getRowContext && contextRow && (
        <LogRowContextModal
          open={contextOpen}
          row={contextRow}
          onClose={onCloseContext}
          getRowContext={(row, options) => getRowContext(row, contextRow, options)}
          getRowContextQuery={getRowContextQuery}
          getLogRowContextUi={getLogRowContextUi}
          logsSortOrder={logsSortOrder}
          timeZone={timeZone}
        />
      )}
      {newLogsPanelEnabled && newLogContextEnabled && getRowContext && contextRow && (
        <LogLineContext
          open={contextOpen}
          log={contextRow}
          onClose={onCloseContext}
          getRowContext={(row, options) => getRowContext(row, contextRow, options)}
          getRowContextQuery={getRowContextQuery}
          getLogRowContextUi={getLogRowContextUi}
          logOptionsStorageKey={SETTING_KEY_ROOT}
          timeZone={timeZone}
          displayedFields={displayedFields}
        />
      )}
      <div className={styles.logsVolumePanel}>
        <PanelChrome
          title={t('explore.unthemed-logs.title-logs-volume', 'Logs volume')}
          collapsible
          loadingState={logsVolumeData?.state}
          collapsed={!logsVolumeEnabled}
          onToggleCollapse={onToggleLogsVolumeCollapse}
        >
          {logsVolumeEnabled && (
            <LogsVolumePanelList
              toggleLegendRef={toggleLegendRef}
              absoluteRange={absoluteRange}
              width={width}
              logsVolumeData={logsVolumeData}
              onUpdateTimeRange={onChangeTime}
              timeZone={timeZone}
              splitOpen={splitOpen}
              onLoadLogsVolume={loadLogsVolumeData}
              onDisplayedSeriesChanged={onDisplayedSeriesChanged}
              eventBus={logsVolumeEventBus}
              onClose={() => onToggleLogsVolumeCollapse(true)}
              logs={logRows}
            />
          )}
        </PanelChrome>
      </div>
      <PanelChrome
        titleItems={[
          visualisationType === 'logs' ? null : (
            <PanelChrome.TitleItem title={t('explore.unthemed-logs.title-feedback', 'Feedback')} key="A">
              <LogsFeedback feedbackUrl="https://forms.gle/5YyKdRQJ5hzq4c289" />
            </PanelChrome.TitleItem>
          ),
        ]}
        title={t('explore.unthemed-logs.title-logs', 'Logs')}
        actions={
          <>
            <div className={styles.visualisationType}>
              <RadioButtonGroup
                className={styles.visualisationTypeRadio}
                options={[
                  {
                    label: t('explore.unthemed-logs.label.logs', 'Logs'),
                    value: 'logs',
                    description: t(
                      'explore.unthemed-logs.description.show-results-in-logs-visualisation',
                      'Show results in logs visualisation'
                    ),
                  },
                  {
                    label: t('explore.unthemed-logs.label.table', 'Table'),
                    value: 'table',
                    description: t(
                      'explore.unthemed-logs.description.show-results-in-table-visualisation',
                      'Show results in table visualisation'
                    ),
                  },
                ]}
                size="sm"
                value={visualisationType}
                onChange={onChangeVisualisation}
              />
            </div>
          </>
        }
        loadingState={loading ? LoadingState.Loading : LoadingState.Done}
      >
        <div className={styles.stickyNavigation}>
          <LogsMetaRow
            meta={logsMeta || []}
            dedupStrategy={dedupStrategy}
            dedupCount={dedupCount}
            displayedFields={displayedFields}
            clearDisplayedFields={clearDisplayedFields}
            defaultDisplayedFields={defaultDisplayedFields}
            visualisationType={visualisationType}
          />
        </div>
        <div className={cx(styles.logsSection, visualisationType === 'table' ? styles.logsTable : undefined)}>
          {enableNewLogsTable && visualisationType === 'table' && hasData && (
            <ExploreLogsTable
              eventBus={eventBus}
              data={panelData}
              isLabelFilterActive={props.isFilterLabelActive}
              timeZone={timeZone}
              buildLinkToLogLine={onTablePermalinkClick}
              externalOptions={tableOptions}
              width={width}
              height={tableHeight}
              onClickFilterLabel={onClickFilterLabel}
              onClickFilterOutLabel={onClickFilterOutLabel}
              onOptionsChange={onTableOptionsChange}
              onChangeTimeRange={onChangeTime}
            />
          )}

          {((visualisationType === 'logs' && !newLogsPanelEnabled) ||
            (visualisationType === 'table' && !enableNewLogsTable)) &&
            hasData && (
              <div className={styles.controlledLogRowsWrapper} data-testid="logRows">
                <ControlledLogRows
                  ref={logsContainerRef}
                  logsTableFrames={props.logsFrames}
                  width={width}
                  updatePanelState={updatePanelState}
                  panelState={panelState?.logs}
                  datasourceType={props.datasourceType}
                  splitOpen={splitOpen}
                  visualisationType={visualisationType}
                  loading={loading}
                  loadMoreLogs={infiniteScrollAvailable ? loadMoreLogs : undefined}
                  range={props.range}
                  pinnedLogs={pinnedLogs}
                  logRows={logRows}
                  deduplicatedRows={dedupedRows}
                  dedupStrategy={dedupStrategy}
                  onClickFilterLabel={onClickFilterLabel}
                  onClickFilterOutLabel={onClickFilterOutLabel}
                  showContextToggle={showContextToggle}
                  getRowContextQuery={getRowContextQuery}
                  showLabels={showLabels}
                  showTime={showTime}
                  enableLogDetails={true}
                  wrapLogMessage={wrapLogMessage}
                  prettifyLogMessage={prettifyLogMessage}
                  timeZone={timeZone}
                  getFieldLinks={getFieldLinks}
                  logsSortOrder={logsSortOrder}
                  displayedFields={displayedFields}
                  onClickShowField={showField}
                  onClickHideField={hideField}
                  app={CoreApp.Explore}
                  onLogRowHover={onLogRowHover}
                  onOpenContext={onOpenContext}
                  onPermalinkClick={onPermalinkClick}
                  permalinkedRowId={panelState?.logs?.id}
                  isFilterLabelActive={props.isFilterLabelActive}
                  onClickFilterString={props.onClickFilterString}
                  onClickFilterOutString={props.onClickFilterOutString}
                  onUnpinLine={onPinToContentOutlineClick}
                  onPinLine={onPinToContentOutlineClick}
                  pinLineButtonTooltipTitle={pinLineButtonTooltipTitle}
                  logsMeta={logsMeta}
                  logOptionsStorageKey={SETTING_KEY_ROOT}
                  onLogOptionsChange={onLogOptionsChange}
                  filterLevels={filterLevels}
                  timeRange={props.range}
                  exploreId={props.exploreId}
                  absoluteRange={props.absoluteRange}
                />
              </div>
            )}
          {newLogsPanelEnabled && visualisationType === 'logs' && (
            <div data-testid="logRows" ref={setLogListContainerElement} className={styles.logRowsWrapper}>
              {logListContainerElement && hasData && (
                <LogList
                  app={CoreApp.Explore}
                  containerElement={logListContainerElement}
                  enableLogDetails={true}
                  dataFrames={props.logsFrames ?? []}
                  dedupStrategy={dedupStrategy}
                  displayedFields={displayedFields}
                  filterLevels={filterLevels}
                  getFieldLinks={getFieldLinks}
                  getRowContextQuery={getRowContextQuery}
                  isLabelFilterActive={props.isFilterLabelActive}
                  loading={loading}
                  loadMore={loadMoreLogs}
                  logOptionsStorageKey={SETTING_KEY_ROOT}
                  logs={dedupedRows}
                  logsMeta={logsMeta}
                  logSupportsContext={showContextToggle}
                  onClickShowField={showField}
                  onClickHideField={hideField}
                  onClickFilterLabel={onClickFilterLabel}
                  onClickFilterOutLabel={onClickFilterOutLabel}
                  onClickFilterString={props.onClickFilterString}
                  onClickFilterOutString={props.onClickFilterOutString}
                  onLogOptionsChange={onLogOptionsChange}
                  onLogLineHover={onLogRowHover}
                  onOpenContext={onOpenContext}
                  onPermalinkClick={onPermalinkClick}
                  onPinLine={onPinToContentOutlineClick}
                  onUnpinLine={onPinToContentOutlineClick}
                  permalinkedLogId={panelState?.logs?.id}
                  pinLineButtonTooltipTitle={pinLineButtonTooltipTitle}
                  pinnedLogs={pinnedLogs}
                  setDisplayedFields={setDisplayedFields}
                  showControls
                  showFieldSelector
                  showTime={showTime}
                  sortOrder={logsSortOrder}
                  timeRange={props.range}
                  timeZone={timeZone}
                  wrapLogMessage={wrapLogMessage}
                />
              )}
            </div>
          )}
        </div>
        {!loading && !hasData && !scanning && (
          <div className={styles.noDataWrapper}>
            <div className={styles.noData}>
              {isMissingTimeField(props.logsFrames) ? (
                <Trans i18nKey="explore.logs.missing-time-field-message">
                  The Logs visualization requires a time field. Add a time-typed column to your query.
                </Trans>
              ) : (
                <>
                  <Trans i18nKey="explore.logs.no-logs-found">No logs found.</Trans>
                  <Button size="sm" variant="secondary" className={styles.scanButton} onClick={onClickScan}>
                    <Trans i18nKey="explore.logs.scan-for-older-logs">Scan for older logs</Trans>
                  </Button>
                </>
              )}
            </div>
          </div>
        )}
        {scanning && (
          <div className={styles.noDataWrapper}>
            <div className={styles.noData}>
              <span>{scanText}</span>
              <Button size="sm" variant="secondary" className={styles.scanButton} onClick={onClickStopScan}>
                <Trans i18nKey="explore.logs.stop-scan">Stop scan</Trans>
              </Button>
            </div>
          </div>
        )}
      </PanelChrome>
    </>
  );
};

export const Logs = withTheme2(UnthemedLogs);

const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean, tableHeight: number) => {
  return {
    noDataWrapper: css({
      display: 'flex',
      justifyContent: 'center',
      width: '100%',
      paddingBottom: theme.spacing(2),
    }),
    noData: css({
      display: 'inline-block',
    }),
    scanButton: css({
      marginLeft: theme.spacing(1),
    }),
    logOptions: css({
      display: 'flex',
      justifyContent: 'space-between',
      alignItems: 'baseline',
      flexWrap: 'wrap',
      backgroundColor: theme.colors.background.primary,
      padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
      borderRadius: theme.shape.radius.default,
      margin: `${theme.spacing(0, 0, 1)}`,
      border: `1px solid ${theme.colors.border.medium}`,
    }),
    headerButton: css({
      margin: `${theme.spacing(0.5, 0, 0, 1)}`,
    }),
    horizontalInlineLabel: css({
      '& > label': {
        marginRight: '0',
      },
    }),
    horizontalInlineSwitch: css({
      padding: `0 ${theme.spacing(1)} 0 0`,
    }),
    radioButtons: css({
      margin: '0',
    }),
    logsSection: css({
      display: 'flex',
      flexDirection: 'row',
      justifyContent: 'space-between',
      position: 'relative',
    }),
    logsTable: css({
      maxHeight: `${tableHeight}px`,
    }),
    scrollableLogRows: css({
      overflowY: 'scroll',
      width: '100%',
      maxHeight: '80vh',
    }),
    logRows: css({
      overflowX: `${wrapLogMessage ? 'unset' : 'scroll'}`,
      overflowY: 'visible',
      width: '100%',
    }),
    controlledLogRowsWrapper: css({
      width: '100%',
      maxHeight: '80vh',
    }),
    logRowsWrapper: css({
      width: '100%',
    }),
    visualisationType: css({
      display: 'flex',
      flex: '1',
      justifyContent: 'flex-end',
    }),
    visualisationTypeRadio: css({
      margin: `0 0 0 ${theme.spacing(1)}`,
    }),
    stickyNavigation: css({
      overflow: 'visible',
      marginBottom: '0px',
    }),
    logsVolumePanel: css({
      marginBottom: theme.spacing(1.5),
    }),
  };
};

const dedupRows = (logRows: LogRowModel[], dedupStrategy: LogsDedupStrategy) => {
  const dedupedRows = dedupLogRows(logRows, dedupStrategy);
  const dedupCount = dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0);
  return { dedupedRows, dedupCount };
};
