/**
 * 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.
 */
/* eslint camelcase: 0 */
import {
  ComponentType,
  memo,
  useCallback,
  useEffect,
  useMemo,
  useState,
} from 'react';
import { bindActionCreators, Dispatch } from 'redux';
import { connect } from 'react-redux';
import {
  useChangeEffect,
  useComponentDidMount,
  usePrevious,
  isMatrixifyEnabled,
  QueryFormData,
  JsonObject,
  MatrixifyFormData,
  DatasourceType,
} from '@superset-ui/core';
import {
  ControlStateMapping,
  ControlPanelState,
} from '@superset-ui/chart-controls';
import { t, styled, css, useTheme } from '@apache-superset/core/ui';
import { logging } from '@apache-superset/core';
import { debounce, isEqual, isObjectLike, omit, pick } from 'lodash';
import { Resizable } from 're-resizable';
import { Tooltip } from '@superset-ui/core/components';
import { usePluginContext } from 'src/components';
import { Global } from '@emotion/react';
import { Icons } from '@superset-ui/core/components/Icons';
import {
  getItem,
  setItem,
  LocalStorageKeys,
} from 'src/utils/localStorageHelpers';
import { RESERVED_CHART_URL_PARAMS, URL_PARAMS } from 'src/constants';
import { QUERY_MODE_REQUISITES } from 'src/explore/constants';
import { areObjectsEqual } from 'src/reduxUtils';
import * as logActions from 'src/logger/actions';
import {
  LOG_ACTIONS_MOUNT_EXPLORER,
  LOG_ACTIONS_CHANGE_EXPLORE_CONTROLS,
} from 'src/logger/LogUtils';
import { ensureAppRoot } from 'src/utils/pathUtils';
import { getUrlParam } from 'src/utils/urlUtils';
import cx from 'classnames';
import * as chartActions from 'src/components/Chart/chartAction';
import { fetchDatasourceMetadata } from 'src/dashboard/actions/datasources';
import { mergeExtraFormData } from 'src/dashboard/components/nativeFilters/utils';
import { postFormData, putFormData } from 'src/explore/exploreUtils/formData';
import { datasourcesActions } from 'src/explore/actions/datasourcesActions';
import { mountExploreUrl } from 'src/explore/exploreUtils';
import { getFormDataFromControls } from 'src/explore/controlUtils';
import * as exploreActions from 'src/explore/actions/exploreActions';
import * as saveModalActions from 'src/explore/actions/saveModalActions';
import { useTabId } from 'src/hooks/useTabId';
import withToasts from 'src/components/MessageToasts/withToasts';
import {
  ChartState,
  Datasource,
  ExplorePageInitialData,
  ExplorePageState,
  SaveActionType,
} from 'src/explore/types';
import { Slice } from 'src/types/Chart';
import { User } from 'src/types/bootstrapTypes';
import ExploreChartPanel from '../ExploreChartPanel';
import ConnectedControlPanelsContainer from '../ControlPanelsContainer';
import SaveModal from '../SaveModal';
import DataSourcePanel from '../DatasourcePanel';
import ConnectedExploreChartHeader from '../ExploreChartHeader';
import ExploreContainer from '../ExploreContainer';

const ExplorePanelContainer = styled.div`
  ${({ theme }) => css`
    text-align: left;
    position: relative;
    width: 100%;
    max-height: 100%;
    background-color: ${theme.colorBgContainer};
    min-height: 0;
    display: flex;
    flex: 1;
    flex-wrap: nowrap;
    border-top: 1px solid ${theme.colorSplit};
    .explore-column {
      display: flex;
      flex-direction: column;
      padding: ${theme.sizeUnit * 2}px 0;
      max-height: 100%;
    }
    .data-source-selection {
      padding: ${theme.sizeUnit * 2}px 0;
      border-right: 1px solid ${theme.colorSplit};
    }
    .main-explore-content {
      flex: 1;
      min-width: ${theme.sizeUnit * 128}px;
      border-left: 1px solid ${theme.colorSplit};
      padding: 0 ${theme.sizeUnit * 4}px;
      .panel {
        margin-bottom: 0;
      }
    }
    .controls-column {
      align-self: flex-start;
      padding: 0;
    }
    .title-container {
      position: relative;
      display: flex;
      flex-direction: row;
      padding: 0 ${theme.sizeUnit * 2}px 0 ${theme.sizeUnit * 4}px;
      justify-content: space-between;
      .horizontal-text {
        font-size: ${theme.fontSize}px;
        line-height: 1.5;
        display: inline-block;
        height: auto;
        overflow: visible;
      }
    }
    .no-show {
      display: none;
    }
    .vertical-text {
      writing-mode: vertical-rl;
      text-orientation: mixed;
    }
    .sidebar {
      height: 100%;
      padding: ${theme.sizeUnit * 2}px;
      width: ${theme.sizeUnit * 8}px;
    }
    .collapse-icon > svg {
      color: ${theme.colorPrimary};
    }
  `};
`;

const updateHistory = debounce(
  async (
    formData,
    datasourceId,
    datasourceType,
    isReplace,
    standalone,
    force,
    title,
    tabId,
  ) => {
    const payload = { ...formData };
    const chartId = formData.slice_id;
    const params = new URLSearchParams(window.location.search);
    const additionalParam = Object.fromEntries(params);

    if (chartId) {
      additionalParam[URL_PARAMS.sliceId.name] = chartId;
    } else {
      additionalParam[URL_PARAMS.datasourceId.name] = datasourceId;
      additionalParam[URL_PARAMS.datasourceType.name] = datasourceType;
    }

    const urlParams = payload?.url_params || {};
    Object.entries(urlParams).forEach(([key, value]) => {
      if (!RESERVED_CHART_URL_PARAMS.includes(key)) {
        additionalParam[key] = value as string;
      }
    });

    try {
      let key: string | null | undefined;
      let stateModifier: 'replaceState' | 'pushState';
      if (isReplace) {
        key = await postFormData(
          datasourceId,
          datasourceType,
          formData,
          chartId,
          tabId,
        );
        stateModifier = 'replaceState';
      } else {
        key = getUrlParam(URL_PARAMS.formDataKey);
        if (key) {
          await putFormData(
            datasourceId,
            datasourceType,
            key,
            formData,
            chartId,
            tabId,
          );
        }
        stateModifier = 'pushState';
      }
      // avoid race condition in case user changes route before explore updates the url
      if (window.location.pathname.startsWith(ensureAppRoot('/explore'))) {
        const url = mountExploreUrl(
          standalone ? URL_PARAMS.standalone.name : 'base',
          {
            [URL_PARAMS.formDataKey.name]: key ?? '',
            ...additionalParam,
          },
          force,
        );
        window.history[stateModifier](payload, title, url);
      }
    } catch (e) {
      logging.warn('Failed at altering browser history', e);
    }
  },
  1000,
);

type DefaultSidebarWidthKey = 'controls_width' | 'datasource_width';

const defaultSidebarsWidth: Record<DefaultSidebarWidthKey, number> = {
  controls_width: 320,
  datasource_width: 300,
};

function getSidebarWidths(
  key: LocalStorageKeys.ControlsWidth | LocalStorageKeys.DatasourceWidth,
): number {
  const defaultKey =
    key === LocalStorageKeys.ControlsWidth
      ? 'controls_width'
      : 'datasource_width';
  return getItem(key, defaultSidebarsWidth[defaultKey]);
}

function setSidebarWidths(
  key: LocalStorageKeys.ControlsWidth | LocalStorageKeys.DatasourceWidth,
  dimension: { width: number },
) {
  const newDimension = Number(getSidebarWidths(key)) + dimension.width;
  setItem(key, newDimension);
}

// Chart types that use aggregation and can have multiple values in tooltips
const AGGREGATED_CHART_TYPES = [
  // Deck.gl aggregated charts
  'deck_screengrid',
  'deck_heatmap',
  'deck_contour',
  'deck_hex',
  'deck_grid',
  // Other aggregated chart types can be added here
  'heatmap',
  'treemap',
  'sunburst',
  'pie',
  'donut',
  'histogram',
  'table',
];

function isAggregatedChartType(vizType: string | undefined): boolean {
  return vizType ? AGGREGATED_CHART_TYPES.includes(vizType) : false;
}

interface ExploreRootState {
  explore: {
    controls: ControlStateMapping;
    slice: Slice | null;
    datasource: Datasource;
    metadata?: ExplorePageInitialData['metadata'];
    hiddenFormData?: Partial<QueryFormData>;
    isDatasourceMetaLoading: boolean;
    isStarred: boolean;
    can_add: boolean;
    can_download: boolean;
    can_overwrite: boolean;
    sliceName?: string;
    triggerRender: boolean;
    standalone: boolean;
    force: boolean;
    form_data?: QueryFormData;
    saveAction?: SaveActionType | null;
  };
  charts: Record<number, ChartState>;
  common: {
    conf: {
      SUPERSET_WEBSERVER_TIMEOUT: number;
    };
  };
  impressionId: string;
  dataMask: Record<number, { ownState?: JsonObject }>;
  reports: JsonObject;
  user: User;
  saveModal: {
    isVisible: boolean;
  };
}

interface OwnProps {
  addDangerToast: (msg: string) => void;
  addSuccessToast?: (msg: string) => void;
}

interface StateProps {
  isDatasourceMetaLoading: boolean;
  datasource: Datasource;
  datasource_type: DatasourceType;
  datasourceId: number;
  dashboardId?: number;
  colorScheme?: string;
  ownColorScheme?: string;
  dashboardColorScheme?: string;
  controls: ControlStateMapping;
  can_add: boolean;
  can_download: boolean;
  can_overwrite: boolean;
  column_formats: JsonObject | null;
  containerId: string;
  isStarred: boolean;
  slice: Slice | null;
  sliceName: string | null;
  triggerRender: boolean;
  form_data: QueryFormData;
  table_name?: string;
  vizType?: string;
  standalone: boolean;
  force: boolean;
  chart: ChartState;
  timeout: number;
  ownState?: JsonObject;
  impressionId: string;
  user: User;
  exploreState: ExplorePageState['explore'];
  reports: JsonObject;
  metadata?: ExplorePageInitialData['metadata'];
  saveAction?: SaveActionType | null;
  isSaveModalVisible: boolean;
}

// Combined actions from all action modules used in Explore
// Note: These modules export both action creators AND action type constants,
// Using a callable signature to allow TypeScript to understand these are functions
interface DispatchProps {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  actions: Record<string, (...args: any[]) => any>;
}

type ExploreViewContainerProps = StateProps & DispatchProps & OwnProps;

function ExploreViewContainer(props: ExploreViewContainerProps) {
  const dynamicPluginContext = usePluginContext();
  const dynamicPlugin = props.vizType
    ? dynamicPluginContext.dynamicPlugins[props.vizType]
    : undefined;
  const isDynamicPluginLoading = dynamicPlugin && dynamicPlugin.mounting;
  const wasDynamicPluginLoading = usePrevious(isDynamicPluginLoading);

  /** the state of controls in the previous render */
  const previousControls = usePrevious(props.controls);
  /** the state of controls last time a query was triggered */
  const [lastQueriedControls, setLastQueriedControls] = useState(
    props.controls,
  );

  const [isCollapsed, setIsCollapsed] = useState(false);
  const [width, setWidth] = useState(
    getSidebarWidths(LocalStorageKeys.DatasourceWidth),
  );
  const tabId = useTabId();

  const theme = useTheme();

  // Capture original title before any effects run
  const originalTitle = useMemo(() => document.title, []);

  // Update document title when slice name changes
  useEffect(() => {
    if (props.sliceName) {
      document.title = props.sliceName;
    }
  }, [props.sliceName]);

  // Restore original title on unmount
  useEffect(
    () => () => {
      document.title =
        originalTitle ||
        theme?.brandAppName ||
        theme?.brandLogoAlt ||
        'Superset';
    },
    [originalTitle, theme?.brandAppName, theme?.brandLogoAlt],
  );

  const addHistory = useCallback(
    async ({ isReplace = false, title } = {}) => {
      const formData = props.dashboardId
        ? {
            ...props.form_data,
            dashboardId: props.dashboardId,
          }
        : props.form_data;
      const { id: datasourceId, type: datasourceType } = props.datasource;

      updateHistory(
        formData,
        datasourceId,
        datasourceType,
        isReplace,
        props.standalone,
        props.force,
        title,
        tabId,
      );
    },
    [
      props.dashboardId,
      props.form_data,
      props.datasource.id,
      props.datasource.type,
      props.standalone,
      props.force,
      tabId,
    ],
  );

  const handlePopstate = useCallback(() => {
    const formData = window.history.state;
    if (formData && Object.keys(formData).length) {
      props.actions.setExploreControls(formData);
      props.actions.postChartFormData(
        formData,
        props.force,
        props.timeout,
        props.chart.id,
      );
    }
  }, [props.actions, props.chart.id, props.timeout]);

  const onQuery = useCallback(() => {
    props.actions.setForceQuery(false);

    // Skip main query if Matrixify is enabled
    if (isMatrixifyEnabled(props.form_data as MatrixifyFormData)) {
      // Set chart to success state since Matrixify will handle its own queries
      props.actions.chartUpdateSucceeded([], props.chart.id);
      props.actions.chartRenderingSucceeded(props.chart.id);

      // Update history and controls
      addHistory();
      setLastQueriedControls(props.controls);
      return;
    }

    // Normal behavior for non-Matrixify
    props.actions.triggerQuery(true, props.chart.id);
    addHistory();
    setLastQueriedControls(props.controls);
  }, [
    props.controls,
    addHistory,
    props.actions,
    props.chart.id,
    props.form_data,
  ]);

  const handleKeydown = useCallback(
    (event: KeyboardEvent) => {
      const controlOrCommand = event.ctrlKey || event.metaKey;
      if (controlOrCommand) {
        const isEnter = event.key === 'Enter' || event.keyCode === 13;
        if (isEnter) {
          onQuery();
        }
        // Note: Ctrl+S save functionality removed due to type incompatibilities
        // between Slice types. Use the save button instead.
      }
    },
    [onQuery],
  );

  function onStop() {
    if (props.chart && props.chart.queryController) {
      props.chart.queryController.abort();
    }
  }

  function toggleCollapse() {
    setIsCollapsed(!isCollapsed);
  }

  useComponentDidMount(() => {
    props.actions.logEvent(
      LOG_ACTIONS_MOUNT_EXPLORER,
      props.slice?.slice_id
        ? {
            slice_id: props.slice.slice_id,
          }
        : {},
    );
  });

  useChangeEffect(tabId, (previous, current) => {
    if (current) {
      addHistory({ isReplace: true });
    }
  });

  const previousHandlePopstate = usePrevious(handlePopstate);
  useEffect(() => {
    if (previousHandlePopstate) {
      window.removeEventListener('popstate', previousHandlePopstate);
    }
    window.addEventListener('popstate', handlePopstate);
    return () => {
      window.removeEventListener('popstate', handlePopstate);
    };
  }, [handlePopstate, previousHandlePopstate]);

  const previousHandleKeyDown = usePrevious(handleKeydown);
  useEffect(() => {
    if (previousHandleKeyDown) {
      window.removeEventListener('keydown', previousHandleKeyDown);
    }
    document.addEventListener('keydown', handleKeydown);
    return () => {
      document.removeEventListener('keydown', handleKeydown);
    };
  }, [handleKeydown, previousHandleKeyDown]);

  useEffect(() => {
    if (wasDynamicPluginLoading && !isDynamicPluginLoading) {
      // reload the controls now that we actually have the control config
      props.actions.dynamicPluginControlsReady();
    }
  }, [isDynamicPluginLoading]);

  useEffect(() => {
    const hasError = Object.values(props.controls).some(
      control =>
        control.validationErrors && control.validationErrors.length > 0,
    );
    if (!hasError) {
      props.actions.triggerQuery(true, props.chart.id);
    }
  }, []);

  const reRenderChart = useCallback(
    (controlsChanged?: string[]) => {
      const newQueryFormData = controlsChanged
        ? {
            ...props.chart.latestQueryFormData,
            ...getFormDataFromControls(pick(props.controls, controlsChanged)),
          }
        : getFormDataFromControls(props.controls);
      props.actions.updateQueryFormData(newQueryFormData, props.chart.id);
      props.actions.renderTriggered(new Date().getTime(), props.chart.id);
      addHistory();
    },
    [
      addHistory,
      props.actions,
      props.chart.id,
      props.chart.latestQueryFormData,
      props.controls,
    ],
  );

  // effect to run when controls change
  useEffect(() => {
    if (
      previousControls &&
      props.chart.latestQueryFormData.viz_type === props.controls.viz_type.value
    ) {
      if (
        props.controls.datasource &&
        (previousControls.datasource == null ||
          props.controls.datasource.value !== previousControls.datasource.value)
      ) {
        // this should really be handled by actions
        fetchDatasourceMetadata(props.form_data.datasource);
      }

      const changedControlKeys = Object.keys(props.controls).filter(
        key =>
          typeof previousControls[key] !== 'undefined' &&
          !areObjectsEqual(
            props.controls[key].value,
            previousControls[key].value,
          ),
      );

      if (changedControlKeys.includes('tooltip_contents')) {
        const tooltipContentsValue = props.controls.tooltip_contents?.value;
        const tooltipContents = Array.isArray(tooltipContentsValue)
          ? tooltipContentsValue
          : [];
        const currentTemplateValue = props.controls.tooltip_template?.value;
        const currentTemplate =
          typeof currentTemplateValue === 'string' ? currentTemplateValue : '';

        if (tooltipContents.length > 0) {
          const getFieldName = (
            item:
              | string
              | {
                  item_type?: string;
                  column_name?: string;
                  metric_name?: string;
                  label?: string;
                },
          ): string | null => {
            if (typeof item === 'string') return item;
            if (item?.item_type === 'column') return item.column_name ?? null;
            if (item?.item_type === 'metric') {
              return item.metric_name || item.label || null;
            }
            return null;
          };

          const vizType = props.form_data?.viz_type || '';
          const isAggregatedChart = isAggregatedChartType(vizType);

          const DEFAULT_TOOLTIP_LIMIT = 10; // Maximum number of values to show in aggregated tooltips

          const fieldNames = tooltipContents
            .map(getFieldName)
            .filter((name): name is string => Boolean(name));
          const missingVariables = fieldNames.filter(
            (fieldName: string) =>
              !currentTemplate.includes(`{{ ${fieldName} }}`) &&
              !currentTemplate.includes(`{{ limit ${fieldName}`),
          );

          if (missingVariables.length > 0) {
            const newVariables = missingVariables.map((fieldName: string) => {
              const item = tooltipContents[fieldNames.indexOf(fieldName)];
              const isColumn =
                (typeof item === 'object' && item?.item_type === 'column') ||
                typeof item === 'string';

              if (isAggregatedChart && isColumn) {
                return `{{ limit ${fieldName} ${DEFAULT_TOOLTIP_LIMIT} }}`;
              }
              return `{{ ${fieldName} }}`;
            });
            const updatedTemplate =
              currentTemplate +
              (currentTemplate ? ' ' : '') +
              newVariables.join(' ');

            props.actions.setControlValue('tooltip_template', updatedTemplate);
          }
        }
      }

      // this should also be handled by the actions that are actually changing the controls
      const displayControlsChanged = changedControlKeys.filter(
        key => props.controls[key].renderTrigger,
      );
      if (displayControlsChanged.length > 0) {
        reRenderChart(displayControlsChanged);
      }
    }
  }, [props.controls, props.ownState]);

  const chartIsStale = useMemo(() => {
    if (lastQueriedControls) {
      const { controls } = props;
      const changedControlKeys = Object.keys(controls).filter(key => {
        const lastControl = lastQueriedControls[key];
        if (typeof lastControl === 'undefined') {
          return false;
        }
        const { value: value1 } = controls[key];
        const { value: value2 } = lastControl;
        if (isObjectLike(value1) && isObjectLike(value2)) {
          return !areObjectsEqual(value1, value2, {
            ignoreFields: ['datasourceWarning'],
          });
        }
        return !isEqual(value1, value2);
      });

      return changedControlKeys.some(
        key =>
          !props.controls[key].renderTrigger &&
          !props.controls[key].dontRefreshOnChange,
      );
    }
    return false;
  }, [lastQueriedControls, props.controls]);

  useChangeEffect(props.saveAction, () => {
    if (
      props.saveAction &&
      ['saveas', 'overwrite'].includes(props.saveAction)
    ) {
      onQuery();
      addHistory({ isReplace: true });
      props.actions.setSaveAction(null);
    }
  });

  const previousOwnState = usePrevious(props.ownState);
  useEffect(() => {
    const strip = (s: JsonObject | undefined) =>
      s && typeof s === 'object' ? omit(s, ['clientView']) : s;
    if (!isEqual(strip(previousOwnState), strip(props.ownState))) {
      onQuery();
      reRenderChart();
    }
  }, [props.ownState]);

  if (chartIsStale) {
    props.actions.logEvent(LOG_ACTIONS_CHANGE_EXPLORE_CONTROLS, {});
  }

  const errorMessage = useMemo(() => {
    // Include all controls with validation errors (for button disabling)
    const controlsWithErrors = Object.values(props.controls).filter(
      control =>
        control.validationErrors && control.validationErrors.length > 0,
    );
    if (controlsWithErrors.length === 0) {
      return null;
    }

    const errorMessages = controlsWithErrors.map(
      control => control.validationErrors,
    );
    const uniqueErrorMessages = [...new Set(errorMessages.flat())];

    const errors = uniqueErrorMessages
      .map(message => {
        const matchingLabels = controlsWithErrors
          .filter(control => control.validationErrors?.includes(message))
          .map(control =>
            typeof control.label === 'function'
              ? control.label(
                  props.exploreState as unknown as ControlPanelState,
                  control,
                )
              : control.label,
          );
        return [matchingLabels, message];
      })
      .map(([labels, message]) => (
        <div key={message}>
          {labels.length > 1 ? t('Controls labeled ') : t('Control labeled ')}
          <strong>{` ${labels.join(', ')}`}</strong>
          <span>: {message}</span>
        </div>
      ));

    let errorMessage;
    if (errors.length > 0) {
      errorMessage = <div style={{ textAlign: 'left' }}>{errors}</div>;
    }
    return errorMessage;
  }, [props.controls]);

  // Error message for Data tab only (excludes matrixify controls)
  const dataTabErrorMessage = useMemo(() => {
    const controlsWithErrors = Object.values(props.controls).filter(
      control =>
        control.validationErrors &&
        control.validationErrors.length > 0 &&
        control.tabOverride !== 'matrixify', // Exclude matrixify controls from Data tab
    );
    if (controlsWithErrors.length === 0) {
      return null;
    }

    const errorMessages = controlsWithErrors.map(
      control => control.validationErrors,
    );
    const uniqueErrorMessages = [...new Set(errorMessages.flat())];

    const errors = uniqueErrorMessages
      .map(message => {
        const matchingLabels = controlsWithErrors
          .filter(control => control.validationErrors?.includes(message))
          .map(control =>
            typeof control.label === 'function'
              ? control.label(
                  props.exploreState as unknown as ControlPanelState,
                  control,
                )
              : control.label,
          );
        return [matchingLabels, message];
      })
      .map(([labels, message]) => (
        <div key={message}>
          {labels.length > 1 ? t('Controls labeled ') : t('Control labeled ')}
          <strong>{` ${labels.join(', ')}`}</strong>
          <span>: {message}</span>
        </div>
      ));

    let dataTabErrorMessage;
    if (errors.length > 0) {
      dataTabErrorMessage = (
        <div
          css={css`
            text-align: 'left';
          `}
        >
          {errors}
        </div>
      );
    }
    return dataTabErrorMessage;
  }, [props.controls]);

  function renderChartContainer() {
    return (
      <ExploreChartPanel
        actions={{
          setForceQuery: props.actions.setForceQuery,
          postChartFormData: props.actions.postChartFormData,
          updateQueryFormData: props.actions.updateQueryFormData,
          setControlValue: (controlName: string, value: any, chartId: number) =>
            props.actions.setControlValue(controlName, value),
        }}
        can_overwrite={props.can_overwrite}
        can_download={props.can_download}
        datasource={props.datasource}
        dashboardId={props.dashboardId}
        column_formats={props.column_formats ?? undefined}
        containerId={props.containerId}
        isStarred={props.isStarred}
        slice={props.slice ?? undefined}
        sliceName={props.sliceName ?? undefined}
        table_name={props.table_name}
        vizType={props.vizType ?? ''}
        form_data={props.form_data}
        ownState={props.ownState}
        standalone={props.standalone}
        force={props.force}
        timeout={props.timeout}
        chart={props.chart}
        triggerRender={props.triggerRender}
        errorMessage={dataTabErrorMessage}
        chartIsStale={chartIsStale}
        onQuery={onQuery}
        exploreState={props.exploreState}
      />
    );
  }

  if (props.standalone) {
    return renderChartContainer();
  }

  return (
    <ExploreContainer>
      <ConnectedExploreChartHeader
        // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Combined actions type is compatible at runtime
        actions={props.actions as any}
        canOverwrite={props.can_overwrite}
        canDownload={props.can_download}
        dashboardId={props.dashboardId}
        colorScheme={props.dashboardColorScheme}
        isStarred={props.isStarred}
        slice={props.slice}
        sliceName={props.sliceName ?? undefined}
        table_name={props.table_name}
        formData={props.form_data}
        chart={props.chart}
        ownState={props.ownState}
        user={props.user}
        saveDisabled={!!errorMessage || props.chart.chartStatus === 'loading'}
        metadata={props.metadata}
        isSaveModalVisible={props.isSaveModalVisible}
      />
      <ExplorePanelContainer id="explore-container">
        <Global
          styles={css`
            .navbar {
              margin-bottom: 0;
            }
            body {
              height: 100vh;
              max-height: 100vh;
              overflow: hidden;
            }
            #app-menu,
            #app {
              flex: 1 1 auto;
            }
            #app {
              flex-basis: 100%;
              overflow: hidden;
              height: 100%;
            }
            #app-menu {
              flex-shrink: 0;
            }
          `}
        />
        <Resizable
          onResizeStop={(evt, direction, ref, d) => {
            setWidth(ref.getBoundingClientRect().width);
            setSidebarWidths(LocalStorageKeys.DatasourceWidth, d);
          }}
          defaultSize={{
            width: getSidebarWidths(LocalStorageKeys.DatasourceWidth),
            height: '100%',
          }}
          minWidth={defaultSidebarsWidth[LocalStorageKeys.DatasourceWidth]}
          maxWidth="33%"
          enable={{ right: true }}
          className={
            isCollapsed ? 'no-show' : 'explore-column data-source-selection'
          }
        >
          <div className="title-container">
            <span className="horizontal-text">{t('Chart Source')}</span>
            <span
              role="button"
              tabIndex={0}
              className="action-button"
              onClick={toggleCollapse}
            >
              <Icons.VerticalAlignTopOutlined
                iconSize="xl"
                css={css`
                  transform: rotate(-90deg);
                `}
                className="collapse-icon"
                iconColor={theme.colorPrimary}
              />
            </span>
          </div>
          {/* eslint-disable @typescript-eslint/no-explicit-any -- DataSourcePanel uses narrower types that are compatible at runtime */}
          <DataSourcePanel
            formData={props.form_data}
            datasource={props.datasource as any}
            controls={props.controls as any}
            actions={props.actions as any}
            width={width}
          />
          {/* eslint-enable @typescript-eslint/no-explicit-any */}
        </Resizable>
        {isCollapsed ? (
          <div
            className="sidebar"
            onClick={toggleCollapse}
            data-test="open-datasource-tab"
            role="button"
            tabIndex={0}
          >
            <span role="button" tabIndex={0} className="action-button">
              <Tooltip title={t('Open Datasource tab')}>
                <Icons.VerticalAlignTopOutlined
                  iconSize="xl"
                  css={css`
                    transform: rotate(90deg);
                  `}
                  className="collapse-icon"
                  iconColor={theme.colorPrimary}
                />
              </Tooltip>
            </span>
          </div>
        ) : null}
        <Resizable
          onResizeStop={(evt, direction, ref, d) =>
            setSidebarWidths(LocalStorageKeys.ControlsWidth, d)
          }
          defaultSize={{
            width: getSidebarWidths(LocalStorageKeys.ControlsWidth),
            height: '100%',
          }}
          minWidth={defaultSidebarsWidth[LocalStorageKeys.ControlsWidth]}
          maxWidth="33%"
          enable={{ right: true }}
          className="col-sm-3 explore-column controls-column"
        >
          <ConnectedControlPanelsContainer
            exploreState={props.exploreState}
            // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Combined actions type is compatible at runtime
            actions={props.actions as any}
            form_data={props.form_data}
            controls={props.controls}
            chart={props.chart}
            datasource_type={props.datasource_type}
            isDatasourceMetaLoading={props.isDatasourceMetaLoading}
            onQuery={onQuery}
            onStop={onStop}
            canStopQuery={props.can_add || props.can_overwrite}
            errorMessage={dataTabErrorMessage}
            buttonErrorMessage={errorMessage}
            chartIsStale={chartIsStale}
          />
        </Resizable>
        <div
          className={cx(
            'main-explore-content',
            isCollapsed ? 'col-sm-9' : 'col-sm-7',
          )}
        >
          {renderChartContainer()}
        </div>
      </ExplorePanelContainer>
      {props.isSaveModalVisible && (
        <SaveModal
          addDangerToast={props.addDangerToast}
          actions={props.actions}
          form_data={props.form_data}
          sliceName={props.sliceName ?? undefined}
          dashboardId={props.dashboardId ?? null}
        />
      )}
    </ExploreContainer>
  );
}

const retainQueryModeRequirements = (
  hiddenFormData: Partial<QueryFormData> | undefined,
): string[] =>
  Object.keys(hiddenFormData ?? {}).filter(
    key => !QUERY_MODE_REQUISITES.has(key),
  );

interface SliceWithSubheader extends Slice {
  form_data?: QueryFormData & {
    subheader?: string;
    subheader_font_size?: number;
  };
}

function patchBigNumberTotalFormData(
  form_data: QueryFormData,
  slice: SliceWithSubheader | null | undefined,
): QueryFormData {
  if (
    form_data.viz_type === 'big_number_total' &&
    !form_data.subtitle &&
    slice?.form_data?.subheader
  ) {
    return { ...form_data, subtitle: slice.form_data.subheader };
  }
  return form_data;
}

function mapStateToProps(state: ExploreRootState) {
  const {
    explore,
    charts,
    common,
    impressionId,
    dataMask,
    reports,
    user,
    saveModal,
  } = state;
  const { controls, slice, datasource, metadata, hiddenFormData } = explore;
  const hasQueryMode = !!controls?.query_mode?.value;
  const fieldsToOmit = hasQueryMode
    ? retainQueryModeRequirements(hiddenFormData)
    : Object.keys(hiddenFormData ?? {});

  const controlsBasedFormData = omit(
    getFormDataFromControls(controls),
    fieldsToOmit,
  ) as QueryFormData;
  const isDeckGLChart = explore.form_data?.viz_type === 'deck_multi';

  const getDeckGLFormData = (): QueryFormData => {
    const formData = { ...controlsBasedFormData } as QueryFormData & {
      layer_filter_scope?: JsonObject;
      filter_data_mapping?: JsonObject;
    };

    if (explore.form_data?.layer_filter_scope) {
      formData.layer_filter_scope = explore.form_data
        .layer_filter_scope as JsonObject;
    }

    if (explore.form_data?.filter_data_mapping) {
      formData.filter_data_mapping = explore.form_data
        .filter_data_mapping as JsonObject;
    }

    return formData;
  };

  const form_data: QueryFormData = isDeckGLChart
    ? getDeckGLFormData()
    : controlsBasedFormData;

  const slice_id = form_data.slice_id ?? slice?.slice_id ?? 0; // 0 - unsaved chart

  // exclude clientView from extra_form_data; keep other ownState pieces
  const ownStateForQuery = omit(dataMask[slice_id]?.ownState, ['clientView']);

  form_data.extra_form_data = mergeExtraFormData(
    { ...form_data.extra_form_data },
    {
      ...ownStateForQuery,
    },
  );
  const chart = charts[slice_id];
  const colorScheme = explore.form_data?.color_scheme;
  const ownColorScheme = explore.form_data?.own_color_scheme;
  const dashboardColorScheme = explore.form_data?.dashboard_color_scheme;

  let dashboardId: number | undefined = Number(explore.form_data?.dashboardId);
  if (Number.isNaN(dashboardId)) {
    dashboardId = undefined;
  }

  if (
    controls &&
    form_data.viz_type === 'big_number_total' &&
    slice?.form_data?.subheader &&
    (!controls.subtitle?.value || controls.subtitle.value === '')
  ) {
    controls.subtitle = {
      ...controls.subtitle,
      value: slice.form_data.subheader,
    };
    if (slice?.form_data?.subheader_font_size) {
      controls.subtitle_font_size = {
        ...controls.subtitle_font_size,
        value: slice.form_data.subheader_font_size,
      };
    }
  }

  const patchedFormData = patchBigNumberTotalFormData(form_data, slice);

  return {
    isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
    datasource,
    datasource_type: datasource.type,
    datasourceId: datasource.id,
    dashboardId,
    colorScheme,
    ownColorScheme,
    dashboardColorScheme,
    controls: explore.controls,
    can_add: !!explore.can_add,
    can_download: !!explore.can_download,
    can_overwrite: !!explore.can_overwrite,
    column_formats: datasource?.column_formats ?? null,
    containerId: slice
      ? `slice-container-${slice.slice_id}`
      : 'slice-container',
    isStarred: explore.isStarred,
    slice,
    sliceName: explore.sliceName ?? slice?.slice_name ?? null,
    triggerRender: explore.triggerRender,
    form_data: patchedFormData,
    table_name: datasource.table_name,
    vizType: form_data.viz_type,
    standalone: !!explore.standalone,
    force: !!explore.force,
    chart,
    timeout: common.conf.SUPERSET_WEBSERVER_TIMEOUT,
    ownState: dataMask[slice_id]?.ownState,
    impressionId,
    user,
    // ExploreRootState['explore'] is compatible with ExplorePageState['explore']
    // but has additional optional fields; casting is safe here
    exploreState: explore as unknown as ExplorePageState['explore'],
    reports,
    metadata,
    saveAction: explore.saveAction,
    isSaveModalVisible: saveModal.isVisible,
  };
}

function mapDispatchToProps(dispatch: Dispatch): DispatchProps {
  const actions = {
    ...exploreActions,
    ...datasourcesActions,
    ...saveModalActions,
    ...chartActions,
    ...logActions,
  };
  return {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Action modules export mixed types (creators + constants)
    actions: bindActionCreators(actions as any, dispatch),
  };
}

// withToasts HOC expects ComponentType<any>, requiring type assertion
// The connected component properly handles StateProps & DispatchProps & OwnProps
export default connect(
  mapStateToProps,
  mapDispatchToProps,
)(withToasts(memo(ExploreViewContainer)) as ComponentType<OwnProps>);
