/**
 * 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 cx from 'classnames';
import { useCallback, useEffect, useRef, useMemo, useState, memo } from 'react';
import PropTypes from 'prop-types';
import { t, logging } from '@superset-ui/core';
import { styled } from '@apache-superset/core/ui';
import { debounce } from 'lodash';
import { useHistory } from 'react-router-dom';
import { bindActionCreators } from 'redux';
import { useDispatch, useSelector } from 'react-redux';

import { exportChart, mountExploreUrl } from 'src/explore/exploreUtils';
import ChartContainer from 'src/components/Chart/ChartContainer';
import {
  StreamingExportModal,
  useStreamingExport,
} from 'src/components/StreamingExportModal';
import {
  LOG_ACTIONS_CHANGE_DASHBOARD_FILTER,
  LOG_ACTIONS_EXPLORE_DASHBOARD_CHART,
  LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART,
  LOG_ACTIONS_EXPORT_XLSX_DASHBOARD_CHART,
  LOG_ACTIONS_FORCE_REFRESH_CHART,
} from 'src/logger/LogUtils';
import { postFormData } from 'src/explore/exploreUtils/formData';
import { URL_PARAMS, DEFAULT_CSV_STREAMING_ROW_THRESHOLD } from 'src/constants';
import { enforceSharedLabelsColorsArray } from 'src/utils/colorScheme';
import exportPivotExcel from 'src/utils/downloadAsPivotExcel';
import {
  convertChartStateToOwnState,
  hasChartStateConverter,
} from '../../../util/chartStateConverter';

import SliceHeader from '../../SliceHeader';
import MissingChart from '../../MissingChart';
import {
  addDangerToast,
  addSuccessToast,
} from '../../../../components/MessageToasts/actions';
import {
  setFocusedFilterField,
  toggleExpandSlice,
  unsetFocusedFilterField,
  updateChartState,
} from '../../../actions/dashboardState';
import { changeFilter } from '../../../actions/dashboardFilters';
import { refreshChart } from '../../../../components/Chart/chartAction';
import { logEvent } from '../../../../logger/actions';
import {
  getActiveFilters,
  getAppliedFilterValues,
} from '../../../util/activeDashboardFilters';
import getFormDataWithExtraFilters from '../../../util/charts/getFormDataWithExtraFilters';
import { PLACEHOLDER_DATASOURCE } from '../../../constants';

const propTypes = {
  id: PropTypes.number.isRequired,
  componentId: PropTypes.string.isRequired,
  dashboardId: PropTypes.number.isRequired,
  width: PropTypes.number.isRequired,
  height: PropTypes.number.isRequired,
  updateSliceName: PropTypes.func.isRequired,
  isComponentVisible: PropTypes.bool,
  handleToggleFullSize: PropTypes.func.isRequired,
  setControlValue: PropTypes.func,
  sliceName: PropTypes.string.isRequired,
  isFullSize: PropTypes.bool,
  extraControls: PropTypes.object,
  isInView: PropTypes.bool,
};

const RESIZE_TIMEOUT = 500;
const DEFAULT_HEADER_HEIGHT = 22;

const ChartWrapper = styled.div`
  overflow: hidden;
  position: relative;

  &.dashboard-chart--overflowable {
    overflow: visible;
  }
`;

const ChartOverlay = styled.div`
  position: absolute;
  top: 0;
  left: 0;
  z-index: 5;
`;

const SliceContainer = styled.div`
  display: flex;
  flex-direction: column;
  max-height: 100%;
`;

const EMPTY_OBJECT = {};
const EMPTY_ARRAY = [];

// Helper function to get chart state with fallback
const getChartStateWithFallback = (chartState, formData, vizType) => {
  if (!hasChartStateConverter(vizType)) {
    return null;
  }

  return (
    chartState?.state || formData.table_state || formData.pivot_table_state
  );
};

// Helper function to create own state with chart state conversion
const createOwnStateWithChartState = (baseOwnState, chartState, vizType) => {
  const state = getChartStateWithFallback(chartState, {}, vizType);

  if (!state) {
    return baseOwnState;
  }

  const convertedState = convertChartStateToOwnState(vizType, state);
  return {
    ...baseOwnState,
    ...convertedState,
    chartState: state,
  };
};

const Chart = props => {
  const dispatch = useDispatch();
  const descriptionRef = useRef(null);
  const headerRef = useRef(null);

  const boundActionCreators = useMemo(
    () =>
      bindActionCreators(
        {
          addSuccessToast,
          addDangerToast,
          toggleExpandSlice,
          changeFilter,
          setFocusedFilterField,
          unsetFocusedFilterField,
          refreshChart,
          logEvent,
        },
        dispatch,
      ),
    [dispatch],
  );

  const chart = useSelector(state => state.charts[props.id] || EMPTY_OBJECT);
  const { queriesResponse, chartUpdateEndTime, chartStatus, annotationQuery } =
    chart;

  const slice = useSelector(
    state => state.sliceEntities.slices[props.id] || EMPTY_OBJECT,
  );
  const editMode = useSelector(state => state.dashboardState.editMode);
  const isExpanded = useSelector(
    state => !!state.dashboardState.expandedSlices[props.id],
  );
  const supersetCanExplore = useSelector(
    state => !!state.dashboardInfo.superset_can_explore,
  );
  const supersetCanShare = useSelector(
    state => !!state.dashboardInfo.superset_can_share,
  );
  const supersetCanCSV = useSelector(
    state => !!state.dashboardInfo.superset_can_csv,
  );
  const timeout = useSelector(
    state => state.dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
  );
  const emitCrossFilters = useSelector(
    state => !!state.dashboardInfo.crossFiltersEnabled,
  );
  const maxRows = useSelector(
    state => state.dashboardInfo.common.conf.SQL_MAX_ROW,
  );
  const streamingThreshold = useSelector(
    state =>
      state.dashboardInfo.common.conf.CSV_STREAMING_ROW_THRESHOLD ||
      DEFAULT_CSV_STREAMING_ROW_THRESHOLD,
  );
  const datasource = useSelector(
    state =>
      (chart &&
        chart.form_data &&
        state.datasources[chart.form_data.datasource]) ||
      PLACEHOLDER_DATASOURCE,
  );
  const dashboardInfo = useSelector(state => state.dashboardInfo);

  const isCached = useMemo(
    // eslint-disable-next-line camelcase
    () => queriesResponse?.map(({ is_cached }) => is_cached) || [],
    [queriesResponse],
  );

  const [descriptionHeight, setDescriptionHeight] = useState(0);
  const [height, setHeight] = useState(props.height);
  const [width, setWidth] = useState(props.width);

  const [isStreamingModalVisible, setIsStreamingModalVisible] = useState(false);
  const {
    progress,
    isExporting,
    startExport,
    cancelExport,
    resetExport,
    retryExport,
  } = useStreamingExport({
    onComplete: () => {
      // Don't show toast here - wait for user to click Download button
    },
    onError: () => {
      boundActionCreators.addDangerToast(t('Export failed - please try again'));
    },
  });

  const handleDownloadComplete = useCallback(() => {
    boundActionCreators.addSuccessToast(t('CSV file downloaded successfully'));
  }, [boundActionCreators]);
  const history = useHistory();
  const resize = useCallback(
    debounce(() => {
      const { width, height } = props;
      setHeight(height);
      setWidth(width);
    }, RESIZE_TIMEOUT),
    [props.width, props.height],
  );

  const ownColorScheme = chart.form_data?.color_scheme;

  const addFilter = useCallback(
    (newSelectedValues = {}) => {
      boundActionCreators.logEvent(LOG_ACTIONS_CHANGE_DASHBOARD_FILTER, {
        id: chart.id,
        columns: Object.keys(newSelectedValues).filter(
          key => newSelectedValues[key] !== null,
        ),
      });
      boundActionCreators.changeFilter(chart.id, newSelectedValues);
    },
    [boundActionCreators.logEvent, boundActionCreators.changeFilter, chart.id],
  );

  // Chart state handler for stateful charts
  const handleChartStateChange = useCallback(
    chartState => {
      if (hasChartStateConverter(slice?.viz_type)) {
        dispatch(updateChartState(props.id, slice.viz_type, chartState));
      }
    },
    [dispatch, props.id, slice?.viz_type],
  );

  useEffect(() => {
    if (isExpanded) {
      const descriptionHeight =
        isExpanded && descriptionRef.current
          ? descriptionRef.current?.offsetHeight
          : 0;
      setDescriptionHeight(descriptionHeight);
    } else {
      setDescriptionHeight(0);
    }
  }, [isExpanded]);

  useEffect(
    () => () => {
      resize.cancel();
    },
    [resize],
  );

  useEffect(() => {
    resize();
  }, [resize, props.isFullSize]);

  const getHeaderHeight = useCallback(() => {
    if (headerRef.current) {
      const computedMarginBottom = getComputedStyle(
        headerRef.current,
      ).getPropertyValue('margin-bottom');
      const marginBottom = parseInt(computedMarginBottom, 10) || 0;
      const computedHeight = getComputedStyle(
        headerRef.current,
      ).getPropertyValue('height');
      const height = parseInt(computedHeight, 10) || DEFAULT_HEADER_HEIGHT;
      return height + marginBottom;
    }
    return DEFAULT_HEADER_HEIGHT;
  }, [headerRef]);

  const getChartHeight = useCallback(() => {
    const headerHeight = getHeaderHeight();
    return Math.max(height - headerHeight - descriptionHeight, 20);
  }, [getHeaderHeight, height, descriptionHeight]);

  const handleFilterMenuOpen = useCallback(
    (chartId, column) => {
      boundActionCreators.setFocusedFilterField(chartId, column);
    },
    [boundActionCreators.setFocusedFilterField],
  );

  const handleFilterMenuClose = useCallback(
    (chartId, column) => {
      boundActionCreators.unsetFocusedFilterField(chartId, column);
    },
    [boundActionCreators.unsetFocusedFilterField],
  );

  const logExploreChart = useCallback(() => {
    boundActionCreators.logEvent(LOG_ACTIONS_EXPLORE_DASHBOARD_CHART, {
      slice_id: slice.slice_id,
      is_cached: isCached,
    });
  }, [boundActionCreators.logEvent, slice.slice_id, isCached]);

  const chartConfiguration = useSelector(
    state => state.dashboardInfo.metadata?.chart_configuration,
  );
  const chartCustomizationItems = useSelector(
    state =>
      state.dashboardInfo.metadata?.chart_customization_config || EMPTY_ARRAY,
  );
  const colorScheme = useSelector(state => state.dashboardState.colorScheme);
  const colorNamespace = useSelector(
    state => state.dashboardState.colorNamespace,
  );
  const datasetsStatus = useSelector(
    state => state.dashboardState.datasetsStatus,
  );
  const allSliceIds = useSelector(state => state.dashboardState.sliceIds);
  const nativeFilters = useSelector(state => state.nativeFilters?.filters);
  const dataMask = useSelector(state => state.dataMask);
  const chartState = useSelector(
    state => state.dashboardState.chartStates?.[props.id],
  );
  const labelsColor = useSelector(
    state => state.dashboardInfo?.metadata?.label_colors || EMPTY_OBJECT,
  );
  const labelsColorMap = useSelector(
    state => state.dashboardInfo?.metadata?.map_label_colors || EMPTY_OBJECT,
  );
  const sharedLabelsColors = useSelector(state =>
    enforceSharedLabelsColorsArray(
      state.dashboardInfo?.metadata?.shared_label_colors,
    ),
  );
  const whatIfModifications = useSelector(
    state => state.dashboardState.whatIfModifications || EMPTY_ARRAY,
  );

  const formData = useMemo(
    () =>
      getFormDataWithExtraFilters({
        chart: { id: chart.id, form_data: chart.form_data }, // avoid passing the whole chart object
        chartConfiguration,
        chartCustomizationItems,
        filters: getAppliedFilterValues(props.id),
        colorScheme,
        colorNamespace,
        sliceId: props.id,
        nativeFilters,
        allSliceIds,
        dataMask,
        extraControls: props.extraControls,
        labelsColor,
        labelsColorMap,
        sharedLabelsColors,
        ownColorScheme,
        whatIfModifications,
      }),
    [
      chart.id,
      chart.form_data,
      chartConfiguration,
      chartCustomizationItems,
      props.id,
      props.extraControls,
      colorScheme,
      colorNamespace,
      nativeFilters,
      allSliceIds,
      dataMask,
      labelsColor,
      labelsColorMap,
      sharedLabelsColors,
      ownColorScheme,
      whatIfModifications,
    ],
  );

  formData.dashboardId = dashboardInfo.id;

  const ownState = useMemo(() => {
    const baseOwnState = dataMask[props.id]?.ownState || EMPTY_OBJECT;
    // Create a chartState-like object that includes state from chartState or formData fallbacks
    const chartStateForConversion = {
      state: getChartStateWithFallback(chartState, formData, slice.viz_type),
    };
    return createOwnStateWithChartState(
      baseOwnState,
      chartStateForConversion,
      slice.viz_type,
    );
  }, [
    dataMask[props.id]?.ownState,
    props.id,
    slice.viz_type,
    chartState?.state,
    formData,
  ]);

  const onExploreChart = useCallback(
    async clickEvent => {
      const isOpenInNewTab =
        clickEvent.shiftKey || clickEvent.ctrlKey || clickEvent.metaKey;
      try {
        const lastTabId = window.localStorage.getItem('last_tab_id');
        const nextTabId = lastTabId
          ? String(Number.parseInt(lastTabId, 10) + 1)
          : undefined;
        const key = await postFormData(
          datasource.id,
          datasource.type,
          formData,
          slice.slice_id,
          nextTabId,
        );
        const url = mountExploreUrl(null, {
          [URL_PARAMS.formDataKey.name]: key,
          [URL_PARAMS.sliceId.name]: slice.slice_id,
        });
        if (isOpenInNewTab) {
          window.open(url, '_blank', 'noreferrer');
        } else {
          history.push(url);
        }
      } catch (error) {
        logging.error(error);
        boundActionCreators.addDangerToast(
          t('An error occurred while opening Explore'),
        );
      }
    },
    [
      datasource.id,
      datasource.type,
      formData,
      slice.slice_id,
      boundActionCreators.addDangerToast,
      history,
    ],
  );

  const exportTable = useCallback(
    (format, isFullCSV, isPivot = false) => {
      const logAction =
        format === 'csv'
          ? LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART
          : LOG_ACTIONS_EXPORT_XLSX_DASHBOARD_CHART;
      boundActionCreators.logEvent(logAction, {
        slice_id: slice.slice_id,
        is_cached: isCached,
      });

      const exportFormData = isFullCSV
        ? { ...formData, row_limit: maxRows }
        : formData;
      const resultType = isPivot ? 'post_processed' : 'full';

      let actualRowCount;
      const isTableViz = formData?.viz_type === 'table';

      if (
        isTableViz &&
        queriesResponse?.length > 1 &&
        queriesResponse[1]?.data?.[0]?.rowcount
      ) {
        actualRowCount = queriesResponse[1].data[0].rowcount;
      } else if (queriesResponse?.[0]?.sql_rowcount != null) {
        actualRowCount = queriesResponse[0].sql_rowcount;
      } else {
        actualRowCount = exportFormData?.row_limit;
      }

      // Handle streaming CSV exports based on row threshold
      const shouldUseStreaming =
        format === 'csv' && !isPivot && actualRowCount >= streamingThreshold;
      let filename;
      if (shouldUseStreaming) {
        const now = new Date();
        const date = now.toISOString().slice(0, 10);
        const time = now.toISOString().slice(11, 19).replace(/:/g, '');
        const timestamp = `_${date}_${time}`;
        const chartName = slice.slice_name || formData.viz_type || 'chart';
        const safeChartName = chartName.replace(/[^a-zA-Z0-9_-]/g, '_');
        filename = `${safeChartName}${timestamp}.csv`;
      }
      const baseOwnState = dataMask[props.id]?.ownState || {};
      const state = getChartStateWithFallback(
        chartState,
        formData,
        slice.viz_type,
      );

      const ownState = state
        ? {
            ...baseOwnState,
            ...convertChartStateToOwnState(slice.viz_type, state),
          }
        : baseOwnState;

      exportChart({
        formData: exportFormData,
        resultType,
        resultFormat: format,
        force: true,
        ownState,
        onStartStreamingExport: shouldUseStreaming
          ? exportParams => {
              setIsStreamingModalVisible(true);
              startExport({
                ...exportParams,
                filename,
                expectedRows: actualRowCount,
              });
            }
          : null,
      });
    },
    [
      slice.slice_id,
      slice.viz_type,
      isCached,
      formData,
      maxRows,
      dataMask[props.id]?.ownState,
      chartState,
      props.id,
      boundActionCreators.logEvent,
      queriesResponse,
      startExport,
      resetExport,
      streamingThreshold,
    ],
  );

  const exportCSV = useCallback(() => {
    exportTable('csv', false);
  }, [exportTable]);

  const exportFullCSV = useCallback(() => {
    exportTable('csv', true);
  }, [exportTable]);

  const exportPivotCSV = useCallback(() => {
    exportTable('csv', false, true);
  }, [exportTable]);

  const exportXLSX = useCallback(() => {
    exportTable('xlsx', false);
  }, [exportTable]);

  const exportFullXLSX = useCallback(() => {
    exportTable('xlsx', true);
  }, [exportTable]);

  const forceRefresh = useCallback(() => {
    boundActionCreators.logEvent(LOG_ACTIONS_FORCE_REFRESH_CHART, {
      slice_id: slice.slice_id,
      is_cached: isCached,
    });
    return boundActionCreators.refreshChart(chart.id, true, props.dashboardId);
  }, [
    boundActionCreators.refreshChart,
    chart.id,
    props.dashboardId,
    slice.slice_id,
    isCached,
    boundActionCreators.logEvent,
  ]);

  if (chart === EMPTY_OBJECT || slice === EMPTY_OBJECT) {
    return <MissingChart height={getChartHeight()} />;
  }

  const isLoading = chartStatus === 'loading';
  const cachedDttm =
    // eslint-disable-next-line camelcase
    queriesResponse?.map(({ cached_dttm }) => cached_dttm) || [];

  return (
    <SliceContainer
      className="chart-slice"
      data-test="chart-grid-component"
      data-test-chart-id={props.id}
      data-test-viz-type={slice.viz_type}
      data-test-chart-name={slice.slice_name}
    >
      <SliceHeader
        ref={headerRef}
        slice={slice}
        isExpanded={isExpanded}
        isCached={isCached}
        cachedDttm={cachedDttm}
        updatedDttm={chartUpdateEndTime}
        toggleExpandSlice={boundActionCreators.toggleExpandSlice}
        forceRefresh={forceRefresh}
        editMode={editMode}
        annotationQuery={annotationQuery}
        logExploreChart={logExploreChart}
        logEvent={boundActionCreators.logEvent}
        onExploreChart={onExploreChart}
        exportCSV={exportCSV}
        exportPivotCSV={exportPivotCSV}
        exportXLSX={exportXLSX}
        exportFullCSV={exportFullCSV}
        exportFullXLSX={exportFullXLSX}
        updateSliceName={props.updateSliceName}
        sliceName={props.sliceName}
        supersetCanExplore={supersetCanExplore}
        supersetCanShare={supersetCanShare}
        supersetCanCSV={supersetCanCSV}
        componentId={props.componentId}
        dashboardId={props.dashboardId}
        filters={getActiveFilters() || EMPTY_OBJECT}
        addSuccessToast={boundActionCreators.addSuccessToast}
        addDangerToast={boundActionCreators.addDangerToast}
        handleToggleFullSize={props.handleToggleFullSize}
        isFullSize={props.isFullSize}
        chartStatus={chartStatus}
        formData={formData}
        width={width}
        height={getHeaderHeight()}
        exportPivotExcel={exportPivotExcel}
      />

      {/*
          This usage of dangerouslySetInnerHTML is safe since it is being used to render
          markdown that is sanitized with nh3. See:
             https://github.com/apache/superset/pull/4390
          and
             https://github.com/apache/superset/pull/23862
        */}
      {isExpanded && slice.description_markeddown && (
        <div
          className="slice_description bs-callout bs-callout-default"
          ref={descriptionRef}
          // eslint-disable-next-line react/no-danger
          dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
          role="complementary"
        />
      )}

      <ChartWrapper
        className={cx('dashboard-chart')}
        aria-label={slice.description}
      >
        {isLoading && (
          <ChartOverlay
            style={{
              width,
              height: getChartHeight(),
            }}
          />
        )}

        <ChartContainer
          width={width}
          height={getChartHeight()}
          addFilter={addFilter}
          onFilterMenuOpen={handleFilterMenuOpen}
          onFilterMenuClose={handleFilterMenuClose}
          annotationData={chart.annotationData}
          chartAlert={chart.chartAlert}
          chartId={props.id}
          chartStatus={chartStatus}
          datasource={datasource}
          dashboardId={props.dashboardId}
          initialValues={EMPTY_OBJECT}
          formData={formData}
          labelsColor={labelsColor}
          labelsColorMap={labelsColorMap}
          ownState={ownState}
          filterState={dataMask[props.id]?.filterState}
          queriesResponse={chart.queriesResponse}
          timeout={timeout}
          triggerQuery={chart.triggerQuery}
          vizType={slice.viz_type}
          setControlValue={props.setControlValue}
          datasetsStatus={datasetsStatus}
          isInView={props.isInView}
          emitCrossFilters={emitCrossFilters}
          onChartStateChange={handleChartStateChange}
        />
      </ChartWrapper>

      <StreamingExportModal
        visible={isStreamingModalVisible}
        onCancel={() => {
          cancelExport();
          setIsStreamingModalVisible(false);
          resetExport();
        }}
        onRetry={retryExport}
        onDownload={handleDownloadComplete}
        progress={progress}
        exportType="csv"
      />
    </SliceContainer>
  );
};

Chart.propTypes = propTypes;

export default memo(Chart, (prevProps, nextProps) => {
  if (prevProps.cacheBusterProp !== nextProps.cacheBusterProp) {
    return false;
  }
  return (
    !nextProps.isComponentVisible ||
    (prevProps.componentId === nextProps.componentId &&
      prevProps.isComponentVisible &&
      prevProps.isInView === nextProps.isInView &&
      prevProps.id === nextProps.id &&
      prevProps.dashboardId === nextProps.dashboardId &&
      prevProps.extraControls === nextProps.extraControls &&
      prevProps.handleToggleFullSize === nextProps.handleToggleFullSize &&
      prevProps.isFullSize === nextProps.isFullSize &&
      prevProps.setControlValue === nextProps.setControlValue &&
      prevProps.sliceName === nextProps.sliceName &&
      prevProps.updateSliceName === nextProps.updateSliceName &&
      prevProps.width === nextProps.width &&
      prevProps.height === nextProps.height)
  );
});
