/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import { t } from '@apache-superset/core/translation';
import {
  DataRecord,
  DataRecordValue,
  getTimeFormatterForGranularity,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
import { isEqual } from 'lodash-es';

import {
  CellClickedEvent,
  SelectionChangedEvent,
} from '@superset-ui/core/components/ThemedAgGridReact';
import {
  AgGridTableChartTransformedProps,
  InputColumn,
  SearchOption,
  SortByItem,
} from './types';
import AgGridDataTable from './AgGridTable';
import { updateTableOwnState } from './utils/externalAPIs';
import TimeComparisonVisibility from './AgGridTable/components/TimeComparisonVisibility';
import { useColDefs } from './utils/useColDefs';
import { buildSelectionCrossFilterDataMask } from './utils/getCrossFilterDataMask';
import { StyledChartContainer } from './styles';
import type { FilterState } from './utils/filterStateManager';

const getGridHeight = (height: number, includeSearch: boolean | undefined) => {
  let calculatedGridHeight = height;
  if (includeSearch) {
    calculatedGridHeight -= 16;
  }
  return calculatedGridHeight - 80;
};

export default function TableChart<D extends DataRecord = DataRecord>(
  props: AgGridTableChartTransformedProps<D> & {},
) {
  const {
    height,
    columns,
    data,
    includeSearch,
    allowRearrangeColumns,
    pageSize,
    serverPagination,
    rowCount,
    setDataMask,
    serverPaginationData,
    slice_id,
    percentMetrics,
    hasServerPageLengthChanged,
    serverPageLength,
    emitCrossFilters,
    filters,
    timeGrain,
    isRawRecords,
    alignPositiveNegative,
    showCellBars,
    isUsingTimeComparison,
    colorPositiveNegative,
    totals,
    showTotals,
    columnColorFormatters,
    basicColorFormatters,
    width,
    onChartStateChange,
    chartState,
    metricSqlExpressions,
    rawSummaryColumns,
    showNumberedColumn,
  } = props;

  const [searchOptions, setSearchOptions] = useState<SearchOption[]>([]);

  // Extract metric column names for SQL conversion
  const metricColumns = useMemo(
    () =>
      columns
        .filter(col => col.isMetric || col.isPercentMetric)
        .map(col => col.key),
    [columns],
  );

  useEffect(() => {
    const options = columns
      .filter(col => col?.dataType === GenericDataType.String)
      .map(column => ({
        value: column.key,
        label: column.label,
      }));

    if (!isEqual(options, searchOptions)) {
      setSearchOptions(options || []);
    }
  }, [columns]);

  // A single effect owns every ownState write derived from render state.
  // updateTableOwnState replaces ownState wholesale, so separate effects that
  // each spread serverPaginationData in the same render would clobber one
  // another's keys: clamping the current page, priming the raw-mode summary
  // columns and nudging a re-query for missing totals must be one combined
  // delta.
  useEffect(() => {
    const nextOwnState = { ...serverPaginationData };
    let changed = false;

    if (serverPagination && serverPaginationData && rowCount !== undefined) {
      const currentPage = serverPaginationData.currentPage ?? 0;
      const currentPageSize = serverPaginationData.pageSize ?? serverPageLength;
      const totalPages = Math.ceil(rowCount / currentPageSize);
      // An empty result set clamps to page zero; a shrunken one clamps to its
      // last remaining page.
      const clampedPage = Math.max(0, Math.min(currentPage, totalPages - 1));
      if (clampedPage !== currentPage) {
        nextOwnState.currentPage = clampedPage;
        changed = true;
      }
    }

    const primed = (serverPaginationData?.rawSummaryColumns ?? []) as string[];
    const requested = Boolean(serverPaginationData?.totalsRequested);
    if (isRawRecords && showTotals && !isEqual(primed, rawSummaryColumns)) {
      nextOwnState.rawSummaryColumns = rawSummaryColumns;
      changed = true;
    }
    // A renderTrigger toggle re-renders without re-querying; requesting totals
    // through ownState dispatches the standard re-query whose buildQuery
    // carries the totals query for the active mode.
    if (showTotals && totals === undefined && !requested) {
      nextOwnState.totalsRequested = true;
      changed = true;
    } else if (!showTotals && requested) {
      nextOwnState.totalsRequested = false;
      changed = true;
    }

    if (changed) {
      updateTableOwnState(setDataMask, nextOwnState);
    }
  }, [
    serverPagination,
    rowCount,
    serverPageLength,
    isRawRecords,
    showTotals,
    totals,
    rawSummaryColumns,
    serverPaginationData,
    setDataMask,
  ]);

  const comparisonColumns = [
    { key: 'all', label: t('Display all') },
    { key: '#', label: '#' },
    { key: '△', label: '△' },
    { key: '%', label: '%' },
  ];

  const [selectedComparisonColumns, setSelectedComparisonColumns] = useState([
    comparisonColumns?.[0]?.key,
  ]);

  const handleColumnStateChange = useCallback(
    (agGridState: Record<string, unknown>) => {
      if (onChartStateChange) {
        onChartStateChange(agGridState);
      }
    },
    [onChartStateChange],
  );

  const handleFilterChanged = useCallback(
    (completeFilterState: FilterState) => {
      if (!serverPagination) return;
      // Sync chartState immediately with the new filter model to prevent stale state
      // This ensures chartState and ownState are in sync
      if (onChartStateChange && chartState) {
        const filterModel =
          completeFilterState.originalFilterModel &&
          Object.keys(completeFilterState.originalFilterModel).length > 0
            ? completeFilterState.originalFilterModel
            : undefined;
        const updatedChartState = {
          ...chartState,
          filterModel,
          timestamp: Date.now(),
        };
        onChartStateChange(updatedChartState);
      }

      // Prepare modified own state for server pagination
      const modifiedOwnState = {
        ...serverPaginationData,
        agGridFilterModel:
          completeFilterState.originalFilterModel &&
          Object.keys(completeFilterState.originalFilterModel).length > 0
            ? completeFilterState.originalFilterModel
            : undefined,
        agGridSimpleFilters: completeFilterState.simpleFilters,
        agGridComplexWhere: completeFilterState.complexWhere,
        agGridHavingClause: completeFilterState.havingClause,
        lastFilteredColumn: completeFilterState.lastFilteredColumn,
        lastFilteredInputPosition: completeFilterState.inputPosition,
        currentPage: 0, // Reset to first page when filtering
        metricSqlExpressions,
      };

      updateTableOwnState(setDataMask, modifiedOwnState);
    },
    [
      setDataMask,
      serverPagination,
      serverPaginationData,
      onChartStateChange,
      chartState,
      metricSqlExpressions,
    ],
  );

  const filteredColumns = useMemo(() => {
    if (!isUsingTimeComparison) {
      return columns;
    }
    if (
      selectedComparisonColumns.length === 0 ||
      selectedComparisonColumns.includes('all')
    ) {
      return columns?.filter(col => col?.config?.visible !== false);
    }

    return columns
      .filter(
        col =>
          !col.originalLabel ||
          (col?.label || '').includes('Main') ||
          selectedComparisonColumns.includes(col.label),
      )
      .filter(col => col?.config?.visible !== false);
  }, [columns, selectedComparisonColumns]);

  const colDefs = useColDefs({
    columns: isUsingTimeComparison
      ? (filteredColumns as InputColumn[])
      : (columns as InputColumn[]),
    data,
    serverPagination,
    serverPaginationData,
    serverPageLength,
    showNumberedColumn: showNumberedColumn && !emitCrossFilters,
    isRawRecords,
    defaultAlignPN: alignPositiveNegative,
    showCellBars,
    colorPositiveNegative,
    columnColorFormatters,
    allowRearrangeColumns,
    basicColorFormatters,
    isUsingTimeComparison,
    emitCrossFilters,
    alignPositiveNegative,
    slice_id,
  });

  const gridHeight = getGridHeight(height, includeSearch);

  const isActiveFilterValue = useCallback(
    function isActiveFilterValue(key: string, val: DataRecordValue) {
      if (!filters || !filters[key]) return false;
      return filters[key].some(filterVal => {
        if (filterVal === val) return true;
        if (filterVal instanceof Date && val instanceof Date) {
          return filterVal.getTime() === val.getTime();
        }
        return false;
      });
    },
    [filters],
  );

  const timestampFormatter = useCallback(
    (value: DataRecordValue) =>
      isRawRecords
        ? String(value ?? '')
        : getTimeFormatterForGranularity(timeGrain)(
            value as number | Date | null | undefined,
          ),
    [timeGrain, isRawRecords],
  );

  const activeColumnRef = useRef<string | null>(null);

  const handleCellClicked = useCallback(
    (event: CellClickedEvent) => {
      if (!emitCrossFilters || !event.column) return;
      const colDef = event.column.getColDef();
      if (colDef.context?.isMetric || colDef.context?.isPercentMetric) return;

      const key = event.column.getColId();
      activeColumnRef.current = key;

      // Re-click on already-filtered single selection → untoggle
      // AG Grid doesn't change selection when re-clicking the same row,
      // so onSelectionChanged won't fire — handle clear directly here
      const selectedNodes = event.api.getSelectedNodes();
      if (
        selectedNodes.length === 1 &&
        selectedNodes[0] === event.node &&
        isActiveFilterValue(key, event.value)
      ) {
        event.node.setSelected(false);
        setDataMask(
          buildSelectionCrossFilterDataMask({
            key,
            values: [],
            timeGrain,
            timestampFormatter,
          }).dataMask,
        );
      }
    },
    [
      emitCrossFilters,
      isActiveFilterValue,
      setDataMask,
      timeGrain,
      timestampFormatter,
    ],
  );

  const handleSelectionChanged = useCallback(
    (event: SelectionChangedEvent) => {
      if (!emitCrossFilters || !activeColumnRef.current) return;

      const key = activeColumnRef.current;
      const selectedRows = event.api.getSelectedRows();
      const values = selectedRows
        .map(row => row[key] as DataRecordValue)
        .filter(v => v != null);

      setDataMask(
        buildSelectionCrossFilterDataMask({
          key,
          values,
          timeGrain,
          timestampFormatter,
        }).dataMask,
      );
    },
    [emitCrossFilters, setDataMask, timeGrain, timestampFormatter],
  );

  const handleServerPaginationChange = useCallback(
    (pageNumber: number, pageSize: number) => {
      const modifiedOwnState = {
        ...serverPaginationData,
        currentPage: pageNumber,
        pageSize,
        lastFilteredColumn: undefined,
        lastFilteredInputPosition: undefined,
      };
      updateTableOwnState(setDataMask, modifiedOwnState);
    },
    [setDataMask],
  );

  const handlePageSizeChange = useCallback(
    (pageSize: number) => {
      const modifiedOwnState = {
        ...serverPaginationData,
        currentPage: 0,
        pageSize,
        lastFilteredColumn: undefined,
        lastFilteredInputPosition: undefined,
      };
      updateTableOwnState(setDataMask, modifiedOwnState);
    },
    [setDataMask],
  );

  const handleChangeSearchCol = (searchCol: string) => {
    if (!isEqual(searchCol, serverPaginationData?.searchColumn)) {
      const modifiedOwnState = {
        ...serverPaginationData,
        searchColumn: searchCol,
        searchText: '',
        lastFilteredColumn: undefined,
        lastFilteredInputPosition: undefined,
      };
      updateTableOwnState(setDataMask, modifiedOwnState);
    }
  };

  const handleSearch = useCallback(
    (searchText: string) => {
      const modifiedOwnState = {
        ...serverPaginationData,
        searchColumn:
          serverPaginationData?.searchColumn || searchOptions[0]?.value,
        searchText,
        currentPage: 0, // Reset to first page when searching
        lastFilteredColumn: undefined,
        lastFilteredInputPosition: undefined,
      };
      updateTableOwnState(setDataMask, modifiedOwnState);
    },
    [setDataMask, searchOptions],
  );

  const handleSortByChange = useCallback(
    (sortBy: SortByItem[]) => {
      if (!serverPagination) return;
      const modifiedOwnState = {
        ...serverPaginationData,
        sortBy,
        lastFilteredColumn: undefined,
        lastFilteredInputPosition: undefined,
      };
      updateTableOwnState(setDataMask, modifiedOwnState);
    },
    [setDataMask, serverPagination],
  );

  const renderTimeComparisonVisibility = (): JSX.Element => (
    <TimeComparisonVisibility
      comparisonColumns={comparisonColumns}
      selectedComparisonColumns={selectedComparisonColumns}
      onSelectionChange={setSelectedComparisonColumns}
    />
  );

  const columnsForKeys = isUsingTimeComparison
    ? (filteredColumns as InputColumn[])
    : (columns as InputColumn[]);
  const descriptionsKey = columnsForKeys
    .map(col => `${col.key}:${col.description ?? ''}`)
    .join('|');

  return (
    <StyledChartContainer height={height}>
      <AgGridDataTable
        gridHeight={gridHeight}
        key={descriptionsKey}
        data={data || []}
        colDefsFromProps={colDefs}
        includeSearch={!!includeSearch}
        allowRearrangeColumns={!!allowRearrangeColumns}
        pagination={!!pageSize && !serverPagination}
        pageSize={pageSize || 0}
        serverPagination={serverPagination}
        rowCount={rowCount}
        onServerPaginationChange={handleServerPaginationChange}
        onServerPageSizeChange={handlePageSizeChange}
        serverPaginationData={serverPaginationData}
        searchOptions={searchOptions}
        onSearchColChange={handleChangeSearchCol}
        onSearchChange={handleSearch}
        onSortChange={handleSortByChange}
        onFilterChanged={handleFilterChanged}
        metricColumns={metricColumns}
        id={slice_id}
        handleCellClicked={handleCellClicked}
        handleSelectionChanged={handleSelectionChanged}
        filters={filters}
        percentMetrics={percentMetrics}
        serverPageLength={serverPageLength}
        hasServerPageLengthChanged={hasServerPageLengthChanged}
        renderTimeComparisonDropdown={
          isUsingTimeComparison ? renderTimeComparisonVisibility : () => null
        }
        cleanedTotals={totals || {}}
        showTotals={
          showTotals && totals !== undefined && Object.keys(totals).length > 0
        }
        width={width}
        onColumnStateChange={handleColumnStateChange}
        chartState={chartState}
      />
    </StyledChartContainer>
  );
}
