/**
 * 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 rison from 'rison';
import { PureComponent, useCallback } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Radio } from '@superset-ui/core/components/Radio';
import {
  css,
  isFeatureEnabled,
  getCurrencySymbol,
  ensureIsArray,
  FeatureFlag,
  styled,
  SupersetClient,
  themeObject,
  t,
  withTheme,
  getClientErrorObject,
  getExtensionsRegistry,
} from '@superset-ui/core';
import Tabs from '@superset-ui/core/components/Tabs';
import WarningIconWithTooltip from '@superset-ui/core/components/WarningIconWithTooltip';
import TableSelector from 'src/components/TableSelector';
import CheckboxControl from 'src/explore/components/controls/CheckboxControl';
import TextControl from 'src/explore/components/controls/TextControl';
import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
import SpatialControl from 'src/explore/components/controls/SpatialControl';
import withToasts from 'src/components/MessageToasts/withToasts';
import CurrencyControl from 'src/explore/components/controls/CurrencyControl';
import {
  Alert,
  AsyncSelect,
  Badge,
  Button,
  Card,
  CertifiedBadge,
  Col,
  Divider,
  EditableTitle,
  FormLabel,
  Icons,
  Loading,
  Row,
  Select,
  Typography,
  Label,
} from '@superset-ui/core/components';
import { FilterableTable } from 'src/components';
import {
  executeQuery,
  formatQuery,
  resetDatabaseState,
} from 'src/database/actions';
import Mousetrap from 'mousetrap';
import { DatabaseSelector } from '../DatabaseSelector';
import CollectionTable from './CollectionTable';
import Fieldset from './Fieldset';
import Field from './Field';
import { fetchSyncedColumns, updateColumns } from './utils';

const extensionsRegistry = getExtensionsRegistry();

const DatasourceContainer = styled.div`
  .change-warning {
    margin: 16px 10px 0;
    color: ${({ theme }) => theme.colorWarning};
  }

  .change-warning .bold {
    font-weight: ${({ theme }) => theme.fontWeightStrong};
  }

  .form-group.has-feedback > .help-block {
    margin-top: 8px;
  }

  .form-group.form-group-md {
    margin-bottom: 8px;
  }
`;

const FlexRowContainer = styled.div`
  align-items: center;
  display: flex;

  svg {
    margin-right: ${({ theme }) => theme.sizeUnit}px;
  }
`;

const StyledTableTabs = styled(Tabs)`
  overflow: visible;
  .ant-tabs-content-holder {
    overflow: visible;
  }
`;

const StyledBadge = styled(Badge)`
  .ant-badge-count {
    line-height: ${({ theme }) => theme.sizeUnit * 4}px;
    height: ${({ theme }) => theme.sizeUnit * 4}px;
    margin-left: ${({ theme }) => theme.sizeUnit}px;
  }
`;

const EditLockContainer = styled.div`
  font-size: ${({ theme }) => theme.fontSizeSM}px;
  display: flex;
  align-items: center;
  a {
    padding: 0 10px;
  }
`;

const ColumnButtonWrapper = styled.div`
  text-align: right;
  ${({ theme }) => `margin-bottom: ${theme.sizeUnit * 2}px`}
`;

const StyledLabelWrapper = styled.div`
  display: flex;
  align-items: center;
  span {
    margin-right: ${({ theme }) => theme.sizeUnit}px;
  }
`;

const StyledColumnsTabWrapper = styled.div`
  .table > tbody > tr > td {
    vertical-align: middle;
  }

  .ant-tag {
    margin-top: ${({ theme }) => theme.sizeUnit}px;
  }
`;

const StyledButtonWrapper = styled.span`
  ${({ theme }) => `
    margin-top: ${theme.sizeUnit * 3}px;
    margin-left: ${theme.sizeUnit * 3}px;
    button>span>:first-of-type {
      margin-right: 0;
    }
  `}
`;

const checkboxGenerator = (d, onChange) => (
  <CheckboxControl value={d} onChange={onChange} />
);
const DATA_TYPES = [
  { value: 'STRING', label: t('STRING') },
  { value: 'NUMERIC', label: t('NUMERIC') },
  { value: 'DATETIME', label: t('DATETIME') },
  { value: 'BOOLEAN', label: t('BOOLEAN') },
];

const TABS_KEYS = {
  SOURCE: 'SOURCE',
  METRICS: 'METRICS',
  COLUMNS: 'COLUMNS',
  CALCULATED_COLUMNS: 'CALCULATED_COLUMNS',
  SETTINGS: 'SETTINGS',
  SPATIAL: 'SPATIAL',
};

const DATASOURCE_TYPES_ARR = [
  { key: 'physical', label: t('Physical (table or view)') },
  { key: 'virtual', label: t('Virtual (SQL)') },
];
const DATASOURCE_TYPES = {};
DATASOURCE_TYPES_ARR.forEach(o => {
  DATASOURCE_TYPES[o.key] = o;
});

function CollectionTabTitle({ title, collection }) {
  return (
    <div
      css={{ display: 'flex', alignItems: 'center' }}
      data-test={`collection-tab-${title}`}
    >
      {title}{' '}
      <StyledBadge count={collection ? collection.length : 0} showZero />
    </div>
  );
}

CollectionTabTitle.propTypes = {
  title: PropTypes.string,
  collection: PropTypes.array,
};

function ColumnCollectionTable({
  columns,
  datasource,
  onColumnsChange,
  onDatasourceChange,
  editableColumnName,
  showExpression,
  allowAddItem,
  allowEditDataType,
  itemGenerator,
  columnLabelTooltips,
}) {
  return (
    <CollectionTable
      tableColumns={
        isFeatureEnabled(FeatureFlag.EnableAdvancedDataTypes)
          ? [
              'column_name',
              'advanced_data_type',
              'type',
              'is_dttm',
              'main_dttm_col',
              'filterable',
              'groupby',
            ]
          : [
              'column_name',
              'type',
              'is_dttm',
              'main_dttm_col',
              'filterable',
              'groupby',
            ]
      }
      sortColumns={
        isFeatureEnabled(FeatureFlag.EnableAdvancedDataTypes)
          ? [
              'column_name',
              'advanced_data_type',
              'type',
              'is_dttm',
              'main_dttm_col',
              'filterable',
              'groupby',
            ]
          : [
              'column_name',
              'type',
              'is_dttm',
              'main_dttm_col',
              'filterable',
              'groupby',
            ]
      }
      allowDeletes
      allowAddItem={allowAddItem}
      itemGenerator={itemGenerator}
      collection={columns}
      columnLabelTooltips={columnLabelTooltips}
      stickyHeader
      expandFieldset={
        <FormContainer>
          <Fieldset compact>
            {showExpression && (
              <Field
                fieldKey="expression"
                label={t('SQL expression')}
                control={
                  <TextAreaControl
                    language="markdown"
                    offerEditInModal={false}
                    resize="vertical"
                  />
                }
              />
            )}
            <Field
              fieldKey="verbose_name"
              label={t('Label')}
              control={
                <TextControl
                  controlId="verbose_name"
                  placeholder={t('Label')}
                />
              }
            />
            <Field
              fieldKey="description"
              label={t('Description')}
              control={
                <TextControl
                  controlId="description"
                  placeholder={t('Description')}
                />
              }
            />
            {allowEditDataType && (
              <Field
                fieldKey="type"
                label={t('Data type')}
                control={
                  <Select
                    ariaLabel={t('Data type')}
                    options={DATA_TYPES}
                    name="type"
                    allowNewOptions
                    allowClear
                  />
                }
              />
            )}
            {isFeatureEnabled(FeatureFlag.EnableAdvancedDataTypes) ? (
              <Field
                fieldKey="advanced_data_type"
                label={t('Advanced data type')}
                control={
                  <TextControl
                    controlId="advanced_data_type"
                    placeholder={t('Advanced Data type')}
                  />
                }
              />
            ) : (
              <></>
            )}
            <Field
              fieldKey="python_date_format"
              label={t('Datetime format')}
              description={
                /* Note the fragmented translations may not work. */
                <div>
                  {t('The pattern of timestamp format. For strings use ')}
                  <Typography.Link href="https://docs.python.org/2/library/datetime.html#strftime-strptime-behavior">
                    {t('Python datetime string pattern')}
                  </Typography.Link>
                  {t(' expression which needs to adhere to the ')}
                  <Typography.Link href="https://en.wikipedia.org/wiki/ISO_8601">
                    {t('ISO 8601')}
                  </Typography.Link>
                  {t(` standard to ensure that the lexicographical ordering
                      coincides with the chronological ordering. If the
                      timestamp format does not adhere to the ISO 8601 standard
                      you will need to define an expression and type for
                      transforming the string into a date or timestamp. Note
                      currently time zones are not supported. If time is stored
                      in epoch format, put \`epoch_s\` or \`epoch_ms\`. If no pattern
                      is specified we fall back to using the optional defaults on a per
                      database/column name level via the extra parameter.`)}
                </div>
              }
              control={
                <TextControl
                  controlId="python_date_format"
                  placeholder="%Y-%m-%d"
                />
              }
            />
            <Field
              fieldKey="certified_by"
              label={t('Certified By')}
              description={t('Person or group that has certified this metric')}
              control={
                <TextControl
                  controlId="certified"
                  placeholder={t('Certified by')}
                />
              }
            />
            <Field
              fieldKey="certification_details"
              label={t('Certification details')}
              description={t('Details of the certification')}
              control={
                <TextControl
                  controlId="certificationDetails"
                  placeholder={t('Certification details')}
                />
              }
            />
          </Fieldset>
        </FormContainer>
      }
      columnLabels={
        isFeatureEnabled(FeatureFlag.EnableAdvancedDataTypes)
          ? {
              column_name: t('Column'),
              advanced_data_type: t('Advanced data type'),
              type: t('Data type'),
              groupby: t('Is dimension'),
              is_dttm: t('Is temporal'),
              main_dttm_col: t('Default datetime'),
              filterable: t('Is filterable'),
            }
          : {
              column_name: t('Column'),
              type: t('Data type'),
              groupby: t('Is dimension'),
              is_dttm: t('Is temporal'),
              main_dttm_col: t('Default datetime'),
              filterable: t('Is filterable'),
            }
      }
      onChange={onColumnsChange}
      itemRenderers={
        isFeatureEnabled(FeatureFlag.EnableAdvancedDataTypes)
          ? {
              column_name: (v, onItemChange, _, record) =>
                editableColumnName ? (
                  <StyledLabelWrapper>
                    {record.is_certified && (
                      <CertifiedBadge
                        certifiedBy={record.certified_by}
                        details={record.certification_details}
                      />
                    )}
                    <EditableTitle
                      canEdit
                      title={v}
                      onSaveTitle={onItemChange}
                    />
                  </StyledLabelWrapper>
                ) : (
                  <StyledLabelWrapper>
                    {record.is_certified && (
                      <CertifiedBadge
                        certifiedBy={record.certified_by}
                        details={record.certification_details}
                      />
                    )}
                    {v}
                  </StyledLabelWrapper>
                ),
              main_dttm_col: (value, _onItemChange, _label, record) => {
                const checked = datasource.main_dttm_col === record.column_name;
                const disabled = !record?.is_dttm;
                return (
                  <Radio
                    aria-label={t(
                      'Set %s as default datetime column',
                      record.column_name,
                    )}
                    data-test={`radio-default-dttm-${record.column_name}`}
                    checked={checked}
                    disabled={disabled}
                    onChange={() =>
                      onDatasourceChange({
                        ...datasource,
                        main_dttm_col: record.column_name,
                      })
                    }
                  />
                );
              },
              type: d => (d ? <Label>{d}</Label> : null),
              advanced_data_type: d => (
                <Label onChange={onColumnsChange}>{d}</Label>
              ),
              is_dttm: checkboxGenerator,
              filterable: checkboxGenerator,
              groupby: checkboxGenerator,
            }
          : {
              column_name: (v, onItemChange, _, record) =>
                editableColumnName ? (
                  <StyledLabelWrapper>
                    {record.is_certified && (
                      <CertifiedBadge
                        certifiedBy={record.certified_by}
                        details={record.certification_details}
                      />
                    )}
                    <TextControl value={v} onChange={onItemChange} />
                  </StyledLabelWrapper>
                ) : (
                  <StyledLabelWrapper>
                    {record.is_certified && (
                      <CertifiedBadge
                        certifiedBy={record.certified_by}
                        details={record.certification_details}
                      />
                    )}
                    {v}
                  </StyledLabelWrapper>
                ),
              main_dttm_col: (value, _onItemChange, _label, record) => {
                const checked = datasource.main_dttm_col === record.column_name;
                const disabled = !record?.is_dttm;
                return (
                  <Radio
                    aria-label={t(
                      'Set %s as default datetime column',
                      record.column_name,
                    )}
                    data-test={`radio-default-dttm-${record.column_name}`}
                    checked={checked}
                    disabled={disabled}
                    onChange={() =>
                      onDatasourceChange({
                        ...datasource,
                        main_dttm_col: record.column_name,
                      })
                    }
                  />
                );
              },
              type: d => (d ? <Label>{d}</Label> : null),
              is_dttm: checkboxGenerator,
              filterable: checkboxGenerator,
              groupby: checkboxGenerator,
            }
      }
    />
  );
}
ColumnCollectionTable.propTypes = {
  columns: PropTypes.array.isRequired,
  datasource: PropTypes.object.isRequired,
  onColumnsChange: PropTypes.func.isRequired,
  onDatasourceChange: PropTypes.func.isRequired,
  editableColumnName: PropTypes.bool,
  showExpression: PropTypes.bool,
  allowAddItem: PropTypes.bool,
  allowEditDataType: PropTypes.bool,
  itemGenerator: PropTypes.func,
};
ColumnCollectionTable.defaultProps = {
  editableColumnName: false,
  showExpression: false,
  allowAddItem: false,
  allowEditDataType: false,
  itemGenerator: () => ({
    column_name: t('<new column>'),
    filterable: true,
    groupby: true,
  }),
};

function StackedField({ label, formElement }) {
  return (
    <div>
      <div>
        <strong>{label}</strong>
      </div>
      <div>{formElement}</div>
    </div>
  );
}

StackedField.propTypes = {
  label: PropTypes.string,
  formElement: PropTypes.node,
};

function FormContainer({ children }) {
  return (
    <Card padded style={{ backgroundColor: themeObject.theme.colorBgLayout }}>
      {children}
    </Card>
  );
}

FormContainer.propTypes = {
  children: PropTypes.node,
};

const propTypes = {
  datasource: PropTypes.object.isRequired,
  onChange: PropTypes.func,
  addSuccessToast: PropTypes.func.isRequired,
  addDangerToast: PropTypes.func.isRequired,
  setIsEditing: PropTypes.func,
};

const defaultProps = {
  onChange: () => {},
  setIsEditing: () => {},
};

function OwnersSelector({ datasource, onChange }) {
  const loadOptions = useCallback((search = '', page, pageSize) => {
    const query = rison.encode({ filter: search, page, page_size: pageSize });
    return SupersetClient.get({
      endpoint: `/api/v1/dataset/related/owners?q=${query}`,
    }).then(response => ({
      data: response.json.result
        .filter(item => item.extra.active)
        .map(item => ({
          value: item.value,
          label: item.text,
        })),
      totalCount: response.json.count,
    }));
  }, []);

  return (
    <AsyncSelect
      ariaLabel={t('Select owners')}
      mode="multiple"
      name="owners"
      value={datasource.owners}
      options={loadOptions}
      onChange={onChange}
      header={<FormLabel>{t('Owners')}</FormLabel>}
      allowClear
    />
  );
}
const ResultTable =
  extensionsRegistry.get('sqleditor.extension.resultTable') ?? FilterableTable;

class DatasourceEditor extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      datasource: {
        ...props.datasource,
        owners: props.datasource.owners.map(owner => ({
          value: owner.value || owner.id,
          label: owner.label || `${owner.first_name} ${owner.last_name}`,
        })),
        metrics: props.datasource.metrics?.map(metric => {
          const {
            certified_by: certifiedByMetric,
            certification_details: certificationDetails,
          } = metric;
          const {
            certification: { details, certified_by: certifiedBy } = {},
            warning_markdown: warningMarkdown,
          } = JSON.parse(metric.extra || '{}') || {};
          return {
            ...metric,
            certification_details: certificationDetails || details,
            warning_markdown: warningMarkdown || '',
            certified_by: certifiedBy || certifiedByMetric,
          };
        }),
      },
      errors: [],
      isSqla:
        props.datasource.datasource_type === 'table' ||
        props.datasource.type === 'table',
      isEditMode: false,
      databaseColumns: props.datasource.columns.filter(col => !col.expression),
      calculatedColumns: props.datasource.columns.filter(
        col => !!col.expression,
      ),
      metadataLoading: false,
      activeTabKey: TABS_KEYS.SOURCE,
      datasourceType: props.datasource.sql
        ? DATASOURCE_TYPES.virtual.key
        : DATASOURCE_TYPES.physical.key,
    };

    this.onChange = this.onChange.bind(this);
    this.onChangeEditMode = this.onChangeEditMode.bind(this);
    this.onDatasourcePropChange = this.onDatasourcePropChange.bind(this);
    this.onDatasourceChange = this.onDatasourceChange.bind(this);
    this.tableChangeAndSyncMetadata =
      this.tableChangeAndSyncMetadata.bind(this);
    this.syncMetadata = this.syncMetadata.bind(this);
    this.setColumns = this.setColumns.bind(this);
    this.validateAndChange = this.validateAndChange.bind(this);
    this.handleTabSelect = this.handleTabSelect.bind(this);
    this.formatSql = this.formatSql.bind(this);
    this.currencies = ensureIsArray(props.currencies).map(currencyCode => ({
      value: currencyCode,
      label: `${getCurrencySymbol({
        symbol: currencyCode,
      })} (${currencyCode})`,
    }));
  }

  onChange() {
    // Emptying SQL if "Physical" radio button is selected
    // Currently the logic to know whether the source is
    // physical or virtual is based on whether SQL is empty or not.
    const { datasourceType, datasource } = this.state;
    const sql =
      datasourceType === DATASOURCE_TYPES.physical.key ? '' : datasource.sql;
    const newDatasource = {
      ...this.state.datasource,
      sql,
      columns: [...this.state.databaseColumns, ...this.state.calculatedColumns],
    };
    this.props.onChange(newDatasource, this.state.errors);
  }

  onChangeEditMode() {
    this.props.setIsEditing(!this.state.isEditMode);
    this.setState(prevState => ({ isEditMode: !prevState.isEditMode }));
  }

  onDatasourceChange(datasource, callback = this.validateAndChange) {
    this.setState({ datasource }, callback);
  }

  onDatasourcePropChange(attr, value) {
    if (value === undefined) return; // if value is undefined do not update state
    const datasource = { ...this.state.datasource, [attr]: value };
    this.setState(
      prevState => ({
        datasource: { ...prevState.datasource, [attr]: value },
      }),
      attr === 'table_name'
        ? this.onDatasourceChange(datasource, this.tableChangeAndSyncMetadata)
        : this.onDatasourceChange(datasource, this.validateAndChange),
    );
  }

  onDatasourceTypeChange(datasourceType) {
    // Call onChange after setting datasourceType to ensure
    // SQL is cleared when switching to a physical dataset
    this.setState({ datasourceType }, this.onChange);
  }

  setColumns(obj) {
    // update calculatedColumns or databaseColumns
    this.setState(obj, this.validateAndChange);
  }

  validateAndChange() {
    this.validate(this.onChange);
  }

  async onQueryRun() {
    this.props.runQuery({
      client_id: this.props.clientId,
      database_id: this.state.datasource.database.id,
      json: true,
      runAsync: false,
      catalog: this.state.datasource.catalog,
      schema: this.state.datasource.schema,
      sql: this.state.datasource.sql,
      tmp_table_name: '',
      select_as_cta: false,
      ctas_method: 'TABLE',
      queryLimit: 25,
      expand_data: true,
    });
  }

  async onQueryFormat() {
    const { datasource } = this.state;
    if (!datasource.sql || !this.state.isEditMode) {
      return;
    }

    try {
      const response = await this.props.formatQuery(datasource.sql);
      this.onDatasourcePropChange('sql', response.json.result);
      this.props.addSuccessToast(t('SQL was formatted'));
    } catch (error) {
      const { error: clientError, statusText } =
        await getClientErrorObject(error);
      this.props.addDangerToast(
        clientError ||
          statusText ||
          t('An error occurred while formatting SQL'),
      );
    }
  }

  getSQLLabUrl() {
    const queryParams = new URLSearchParams({
      dbid: this.state.datasource.database.id,
      sql: this.state.datasource.sql,
      name: this.state.datasource.datasource_name,
      schema: this.state.datasource.schema,
      autorun: true,
      isDataset: true,
    });
    return `/sqllab/?${queryParams.toString()}`;
  }

  openOnSqlLab() {
    window.open(this.getSQLLabUrl(), '_blank', 'noopener,noreferrer');
  }

  tableChangeAndSyncMetadata() {
    this.validate(() => {
      this.syncMetadata();
      this.onChange();
    });
  }

  async formatSql() {
    const { datasource } = this.state;
    if (!datasource.sql) {
      return;
    }

    try {
      const response = await SupersetClient.post({
        endpoint: '/api/v1/sql/format',
        body: JSON.stringify({ sql: datasource.sql }),
        headers: { 'Content-Type': 'application/json' },
      });
      this.onDatasourcePropChange('sql', response.json.result);
      this.props.addSuccessToast(t('SQL was formatted'));
    } catch (error) {
      const { error: clientError, statusText } =
        await getClientErrorObject(error);
      this.props.addDangerToast(
        clientError ||
          statusText ||
          t('An error occurred while formatting SQL'),
      );
    }
  }

  async syncMetadata() {
    const { datasource } = this.state;
    this.setState({ metadataLoading: true });
    try {
      const newCols = await fetchSyncedColumns(datasource);
      const columnChanges = updateColumns(
        datasource.columns,
        newCols,
        this.props.addSuccessToast,
      );
      this.setColumns({
        databaseColumns: columnChanges.finalColumns.filter(
          col => !col.expression, // remove calculated columns
        ),
      });
      this.props.addSuccessToast(t('Metadata has been synced'));
      this.setState({ metadataLoading: false });
    } catch (error) {
      const { error: clientError, statusText } =
        await getClientErrorObject(error);
      this.props.addDangerToast(
        clientError || statusText || t('An error has occurred'),
      );
      this.setState({ metadataLoading: false });
    }
  }

  findDuplicates(arr, accessor) {
    const seen = {};
    const dups = [];
    arr.forEach(obj => {
      const item = accessor(obj);
      if (item in seen) {
        dups.push(item);
      } else {
        seen[item] = null;
      }
    });
    return dups;
  }

  validate(callback) {
    let errors = [];
    let dups;
    const { datasource } = this.state;

    // Looking for duplicate column_name
    dups = this.findDuplicates(datasource.columns, obj => obj.column_name);
    errors = errors.concat(
      dups.map(name => t('Column name [%s] is duplicated', name)),
    );

    // Looking for duplicate metric_name
    dups = this.findDuplicates(datasource.metrics, obj => obj.metric_name);
    errors = errors.concat(
      dups.map(name => t('Metric name [%s] is duplicated', name)),
    );

    // Making sure calculatedColumns have an expression defined
    const noFilterCalcCols = this.state.calculatedColumns.filter(
      col => !col.expression && !col.json,
    );
    errors = errors.concat(
      noFilterCalcCols.map(col =>
        t('Calculated column [%s] requires an expression', col.column_name),
      ),
    );

    // validate currency code
    try {
      this.state.datasource.metrics?.forEach(
        metric =>
          metric.currency?.symbol &&
          new Intl.NumberFormat('en-US', {
            style: 'currency',
            currency: metric.currency.symbol,
          }),
      );
    } catch {
      errors = errors.concat([t('Invalid currency code in saved metrics')]);
    }

    this.setState({ errors }, callback);
  }

  handleTabSelect(activeTabKey) {
    this.setState({ activeTabKey });
  }

  sortMetrics(metrics) {
    return metrics.sort(({ id: a }, { id: b }) => b - a);
  }

  renderSettingsFieldset() {
    const { datasource } = this.state;
    return (
      <Fieldset
        title={t('Basic')}
        item={datasource}
        onChange={this.onDatasourceChange}
      >
        <Field
          fieldKey="description"
          label={t('Description')}
          control={
            <TextAreaControl
              language="markdown"
              offerEditInModal={false}
              resize="vertical"
            />
          }
        />
        <Field
          fieldKey="default_endpoint"
          label={t('Default URL')}
          description={t(
            `Default URL to redirect to when accessing from the dataset list page.
            Accepts relative URLs such as <span style=„white-space: nowrap;”>/superset/dashboard/{id}/</span>`,
          )}
          control={<TextControl controlId="default_endpoint" />}
        />
        <Field
          inline
          fieldKey="filter_select_enabled"
          label={t('Autocomplete filters')}
          description={t('Whether to populate autocomplete filters options')}
          control={<CheckboxControl />}
        />
        {this.state.isSqla && (
          <Field
            fieldKey="fetch_values_predicate"
            label={t('Autocomplete query predicate')}
            description={t(
              'When using "Autocomplete filters", this can be used to improve performance ' +
                'of the query fetching the values. Use this option to apply a ' +
                'predicate (WHERE clause) to the query selecting the distinct ' +
                'values from the table. Typically the intent would be to limit the scan ' +
                'by applying a relative time filter on a partitioned or indexed time-related field.',
            )}
            control={
              <TextAreaControl
                language="sql"
                controlId="fetch_values_predicate"
                minLines={5}
                resize="vertical"
              />
            }
          />
        )}
        {this.state.isSqla && (
          <Field
            fieldKey="extra"
            label={t('Extra')}
            description={t(
              'Extra data to specify table metadata. Currently supports ' +
                'metadata of the format: `{ "certification": { "certified_by": ' +
                '"Data Platform Team", "details": "This table is the source of truth." ' +
                '}, "warning_markdown": "This is a warning." }`.',
            )}
            control={
              <TextAreaControl
                controlId="extra"
                language="json"
                offerEditInModal={false}
                resize="vertical"
              />
            }
          />
        )}
        <OwnersSelector
          datasource={datasource}
          onChange={newOwners => {
            this.onDatasourceChange({ ...datasource, owners: newOwners });
          }}
        />
      </Fieldset>
    );
  }

  renderAdvancedFieldset() {
    const { datasource } = this.state;
    return (
      <Fieldset
        title={t('Advanced')}
        item={datasource}
        onChange={this.onDatasourceChange}
      >
        <Field
          fieldKey="cache_timeout"
          label={t('Cache timeout')}
          description={t(
            'The duration of time in seconds before the cache is invalidated. Set to -1 to bypass the cache.',
          )}
          control={<TextControl controlId="cache_timeout" />}
        />
        <Field
          fieldKey="offset"
          label={t('Hours offset')}
          control={<TextControl controlId="offset" />}
          description={t(
            'The number of hours, negative or positive, to shift the time column. This can be used to move UTC time to local time.',
          )}
        />
        {this.state.isSqla && (
          <Field
            fieldKey="template_params"
            label={t('Template parameters')}
            description={t(
              'A set of parameters that become available in the query using Jinja templating syntax',
            )}
            control={<TextControl controlId="template_params" />}
          />
        )}
        <Field
          inline
          fieldKey="normalize_columns"
          label={t('Normalize column names')}
          description={t(
            'Allow column names to be changed to case insensitive format, if supported (e.g. Oracle, Snowflake).',
          )}
          control={<CheckboxControl controlId="normalize_columns" />}
        />
        <Field
          inline
          fieldKey="always_filter_main_dttm"
          label={t('Always filter main datetime column')}
          description={t(
            `When the secondary temporal columns are filtered, apply the same filter to the main datetime column.`,
          )}
          control={<CheckboxControl controlId="always_filter_main_dttm" />}
        />
      </Fieldset>
    );
  }

  renderSpatialTab() {
    const { datasource } = this.state;
    const { spatials, all_cols: allCols } = datasource;

    return {
      key: TABS_KEYS.SPATIAL,
      label: <CollectionTabTitle collection={spatials} title={t('Spatial')} />,
      children: (
        <CollectionTable
          tableColumns={['name', 'config']}
          onChange={this.onDatasourcePropChange.bind(this, 'spatials')}
          itemGenerator={() => ({
            name: t('<new spatial>'),
            type: t('<no type>'),
            config: null,
          })}
          collection={spatials}
          allowDeletes
          itemRenderers={{
            name: (d, onChange) => (
              <EditableTitle canEdit title={d} onSaveTitle={onChange} />
            ),
            config: (v, onChange) => (
              <SpatialControl value={v} onChange={onChange} choices={allCols} />
            ),
          }}
        />
      ),
    };
  }

  renderSqlEditorOverlay = () => (
    <div
      css={theme => css`
        position: absolute;
        background: ${theme.colorBgLayout};
        align-items: center;
        display: flex;
        height: 100%;
        width: 100%;
        justify-content: center;
      `}
    >
      <div>
        <Loading position="inline-centered" />
        <span
          css={theme => css`
            display: block;
            margin: ${theme.sizeUnit * 4}px auto;
            width: fit-content;
            color: ${theme.colorText};
          `}
        >
          {t('We are working on your query')}
        </span>
      </div>
    </div>
  );

  renderOpenInSqlLabLink(isError = false) {
    return (
      <a
        href={this.getSQLLabUrl()}
        target="_blank"
        rel="noopener noreferrer"
        css={theme => css`
          color: ${isError ? theme.colorErrorText : theme.colorText};
          font-size: ${theme.fontSizeSM}px;
          text-decoration: underline;
        `}
      >
        {t('Open in SQL lab')}
      </a>
    );
  }

  renderSqlErrorMessage = () => (
    <span
      css={theme => css`
        font-size: ${theme.fontSizeSM}px;
        color: ${theme.colorErrorText};
      `}
    >
      {this.props.database?.error && t('Error executing query. ')}
      {this.renderOpenInSqlLabLink(true)}
      {t(' to check for details.')}
    </span>
  );

  renderSourceFieldset() {
    const { datasource } = this.state;

    return (
      <div>
        <EditLockContainer>
          <span
            css={theme => css`
              color: ${theme.colorTextTertiary};
            `}
            role="button"
            tabIndex={0}
            onClick={this.onChangeEditMode}
          >
            {this.state.isEditMode ? (
              <Icons.UnlockOutlined
                iconSize="xl"
                css={theme => css`
                  margin: auto ${theme.sizeUnit}px auto 0;
                `}
              />
            ) : (
              <Icons.LockOutlined
                iconSize="xl"
                css={theme => ({
                  margin: `auto ${theme.sizeUnit}px auto 0`,
                })}
              />
            )}
          </span>
          {!this.state.isEditMode && (
            <div>{t('Click the lock to make changes.')}</div>
          )}
          {this.state.isEditMode && (
            <div>{t('Click the lock to prevent further changes.')}</div>
          )}
        </EditLockContainer>
        <div
          css={theme => css`
            margin-top: ${theme.sizeUnit * 3}px;
          `}
        >
          {DATASOURCE_TYPES_ARR.map(type => (
            <Radio
              key={type.key}
              value={type.key}
              inline
              onChange={this.onDatasourceTypeChange.bind(this, type.key)}
              checked={this.state.datasourceType === type.key}
              disabled={!this.state.isEditMode}
            >
              {type.label}
            </Radio>
          ))}
        </div>
        <Divider />
        <Fieldset item={datasource} onChange={this.onDatasourceChange} compact>
          {this.state.datasourceType === DATASOURCE_TYPES.virtual.key && (
            <div>
              {this.state.isSqla && (
                <>
                  <Col xs={24} md={12}>
                    <Field
                      fieldKey="databaseSelector"
                      label={t('Virtual')}
                      control={
                        <div css={{ marginTop: 8 }}>
                          <DatabaseSelector
                            db={datasource?.database}
                            catalog={datasource.catalog}
                            schema={datasource.schema}
                            onCatalogChange={catalog =>
                              this.state.isEditMode &&
                              this.onDatasourcePropChange('catalog', catalog)
                            }
                            onSchemaChange={schema =>
                              this.state.isEditMode &&
                              this.onDatasourcePropChange('schema', schema)
                            }
                            onDbChange={database =>
                              this.state.isEditMode &&
                              this.onDatasourcePropChange('database', database)
                            }
                            formMode={false}
                            handleError={this.props.addDangerToast}
                            readOnly={!this.state.isEditMode}
                          />
                        </div>
                      }
                    />
                    <div css={{ width: 'calc(100% - 34px)', marginTop: -16 }}>
                      <Field
                        fieldKey="table_name"
                        label={t('Name')}
                        control={
                          <TextControl
                            controlId="table_name"
                            onChange={table => {
                              this.onDatasourcePropChange('table_name', table);
                            }}
                            placeholder={t('Dataset name')}
                            disabled={!this.state.isEditMode}
                          />
                        }
                      />
                    </div>
                  </Col>
                  <Field
                    fieldKey="sql"
                    label={t('SQL')}
                    description={t(
                      'When specifying SQL, the datasource acts as a view. ' +
                        'Superset will use this statement as a subquery while grouping and filtering ' +
                        'on the generated parent queries.' +
                        'If changes are made to your SQL query, ' +
                        'columns in your dataset will be synced when saving the dataset.',
                    )}
                    control={
                      this.props.database?.isLoading ? (
                        <>
                          {this.renderSqlEditorOverlay()}
                          <TextAreaControl
                            hotkeys={[
                              {
                                name: 'formatQuery',
                                key: 'ctrl+shift+f',
                                descr: t('Format SQL query'),
                                func: () => {
                                  this.onQueryFormat();
                                },
                              },
                            ]}
                            language="sql"
                            offerEditInModal={false}
                            minLines={10}
                            maxLines={Infinity}
                            readOnly={!this.state.isEditMode}
                            resize="both"
                          />
                        </>
                      ) : (
                        <TextAreaControl
                          css={theme => css`
                            margin-top: ${theme.sizeUnit * 3}px;
                          `}
                          hotkeys={[
                            {
                              name: 'formatQuery',
                              key: 'ctrl+shift+f',
                              descr: t('Format SQL query'),
                              func: () => {
                                this.onQueryFormat();
                              },
                            },
                          ]}
                          language="sql"
                          offerEditInModal={false}
                          minLines={10}
                          maxLines={Infinity}
                          readOnly={!this.state.isEditMode}
                          resize="both"
                        />
                      )
                    }
                    additionalControl={
                      <div
                        css={css`
                          position: absolute;
                          right: 0;
                          top: 0;
                          z-index: 2;
                          display: flex;
                        `}
                      >
                        <Button
                          disabled={this.props.database?.isLoading}
                          tooltip={t('Open SQL Lab in a new tab')}
                          buttonStyle="secondary"
                          onClick={() => {
                            this.openOnSqlLab();
                          }}
                          icon={<Icons.ExportOutlined iconSize="s" />}
                        />
                        <Button
                          disabled={this.props.database?.isLoading}
                          tooltip={t('Run query')}
                          buttonStyle="primary"
                          onClick={() => {
                            this.onQueryRun();
                          }}
                          icon={<Icons.CaretRightFilled iconSize="s" />}
                        />
                      </div>
                    }
                  />
                  {this.props.database?.queryResult && (
                    <>
                      <div
                        css={theme => css`
                          margin-bottom: ${theme.sizeUnit}px;
                        `}
                      >
                        <span
                          css={theme => css`
                            color: ${theme.colorText};
                            font-size: ${theme.fontSizeSM}px;
                          `}
                        >
                          {t(
                            'In this view you can preview the first 25 rows. ',
                          )}
                        </span>
                        {this.renderOpenInSqlLabLink()}
                        <span
                          css={theme => css`
                            color: ${theme.colorText};
                            font-size: ${theme.fontSizeSM}px;
                          `}
                        >
                          {t(' to see details.')}
                        </span>
                      </div>
                      <ResultTable
                        data={this.props.database?.queryResult.data}
                        queryId={this.props.database?.queryResult.query.id}
                        orderedColumnKeys={this.props.database?.queryResult.columns.map(
                          col => col.column_name,
                        )}
                        expandedColumns={
                          this.props.database?.queryResult.expandedColumns
                        }
                        height={300}
                        allowHTML
                      />
                    </>
                  )}
                  {this.props.database?.error && this.renderSqlErrorMessage()}
                </>
              )}
            </div>
          )}
          {this.state.datasourceType === DATASOURCE_TYPES.physical.key && (
            <Col xs={24} md={12}>
              {this.state.isSqla && (
                <Field
                  fieldKey="tableSelector"
                  label={t('Physical')}
                  control={
                    <div css={{ marginTop: 8 }}>
                      <TableSelector
                        clearable={false}
                        database={{
                          ...datasource.database,
                          database_name:
                            datasource.database?.database_name ||
                            datasource.database?.name,
                        }}
                        dbId={datasource.database?.id}
                        handleError={this.props.addDangerToast}
                        catalog={datasource.catalog}
                        schema={datasource.schema}
                        sqlLabMode={false}
                        tableValue={datasource.table_name}
                        onCatalogChange={
                          this.state.isEditMode
                            ? catalog =>
                                this.onDatasourcePropChange('catalog', catalog)
                            : undefined
                        }
                        onSchemaChange={
                          this.state.isEditMode
                            ? schema =>
                                this.onDatasourcePropChange('schema', schema)
                            : undefined
                        }
                        onDbChange={
                          this.state.isEditMode
                            ? database =>
                                this.onDatasourcePropChange(
                                  'database',
                                  database,
                                )
                            : undefined
                        }
                        onTableSelectChange={
                          this.state.isEditMode
                            ? table =>
                                this.onDatasourcePropChange('table_name', table)
                            : undefined
                        }
                        readOnly={!this.state.isEditMode}
                      />
                    </div>
                  }
                  description={t(
                    'The pointer to a physical table (or view). Keep in mind that the chart is ' +
                      'associated to this Superset logical table, and this logical table points ' +
                      'the physical table referenced here.',
                  )}
                />
              )}
            </Col>
          )}
        </Fieldset>
      </div>
    );
  }

  renderErrors() {
    if (this.state.errors.length > 0) {
      return (
        <Alert
          css={theme => ({ marginBottom: theme.sizeUnit * 4 })}
          type="error"
          message={
            <>
              {this.state.errors.map(err => (
                <div key={err}>{err}</div>
              ))}
            </>
          }
        />
      );
    }
    return null;
  }

  renderMetricCollection() {
    const { datasource } = this.state;
    const { metrics } = datasource;
    const sortedMetrics = metrics?.length ? this.sortMetrics(metrics) : [];
    return (
      <CollectionTable
        tableColumns={['metric_name', 'verbose_name', 'expression']}
        sortColumns={['metric_name', 'verbose_name', 'expression']}
        columnLabels={{
          metric_name: t('Metric Key'),
          verbose_name: t('Label'),
          expression: t('SQL expression'),
        }}
        columnLabelTooltips={{
          metric_name: t(
            'This field is used as a unique identifier to attach ' +
              'the metric to charts. It is also used as the alias in the ' +
              'SQL query.',
          ),
        }}
        expandFieldset={
          <FormContainer>
            <Fieldset compact>
              <Field
                fieldKey="description"
                label={t('Description')}
                control={
                  <TextControl
                    controlId="description"
                    placeholder={t('Description')}
                  />
                }
              />
              <Field
                fieldKey="d3format"
                label={t('D3 format')}
                control={
                  <TextControl controlId="d3format" placeholder="%y/%m/%d" />
                }
              />
              <Field
                fieldKey="currency"
                label={t('Metric currency')}
                control={
                  <CurrencyControl
                    currencySelectOverrideProps={{
                      placeholder: t('Select or type currency symbol'),
                    }}
                    symbolSelectAdditionalStyles={css`
                      max-width: 30%;
                    `}
                  />
                }
              />
              <Field
                label={t('Certified by')}
                fieldKey="certified_by"
                description={t(
                  'Person or group that has certified this metric',
                )}
                control={
                  <TextControl
                    controlId="certified_by"
                    placeholder={t('Certified by')}
                  />
                }
              />
              <Field
                label={t('Certification details')}
                fieldKey="certification_details"
                description={t('Details of the certification')}
                control={
                  <TextControl
                    controlId="certification_details"
                    placeholder={t('Certification details')}
                  />
                }
              />
              <Field
                label={t('Warning')}
                fieldKey="warning_markdown"
                description={t('Optional warning about use of this metric')}
                control={
                  <TextAreaControl
                    controlId="warning_markdown"
                    language="markdown"
                    offerEditInModal={false}
                    resize="vertical"
                  />
                }
              />
            </Fieldset>
          </FormContainer>
        }
        collection={sortedMetrics}
        allowAddItem
        onChange={this.onDatasourcePropChange.bind(this, 'metrics')}
        itemGenerator={() => ({
          metric_name: t('<new metric>'),
          verbose_name: '',
          expression: '',
        })}
        itemCellProps={{
          expression: () => ({
            width: '240px',
          }),
        }}
        itemRenderers={{
          metric_name: (v, onChange, _, record) => (
            <FlexRowContainer>
              {record.is_certified && (
                <CertifiedBadge
                  certifiedBy={record.certified_by}
                  details={record.certification_details}
                />
              )}
              {record.warning_markdown && (
                <WarningIconWithTooltip
                  warningMarkdown={record.warning_markdown}
                />
              )}
              <EditableTitle
                canEdit
                title={v}
                onSaveTitle={onChange}
                maxWidth={300}
              />
            </FlexRowContainer>
          ),
          verbose_name: (v, onChange) => (
            <TextControl canEdit value={v} onChange={onChange} />
          ),
          expression: (v, onChange) => (
            <TextAreaControl
              canEdit
              initialValue={v}
              onChange={onChange}
              extraClasses={['datasource-sql-expression']}
              language="sql"
              offerEditInModal={false}
              minLines={5}
              textAreaStyles={{ minWidth: '200px', maxWidth: '450px' }}
              resize="both"
            />
          ),
          description: (v, onChange, label) => (
            <StackedField
              label={label}
              formElement={<TextControl value={v} onChange={onChange} />}
            />
          ),
          d3format: (v, onChange, label) => (
            <StackedField
              label={label}
              formElement={<TextControl value={v} onChange={onChange} />}
            />
          ),
        }}
        allowDeletes
        stickyHeader
      />
    );
  }

  render() {
    const { datasource, activeTabKey } = this.state;
    const { metrics } = datasource;
    const sortedMetrics = metrics?.length ? this.sortMetrics(metrics) : [];
    const { theme } = this.props;

    return (
      <DatasourceContainer data-test="datasource-editor">
        {this.renderErrors()}
        <Alert
          css={theme => ({ marginBottom: theme.sizeUnit * 4 })}
          type="warning"
          message={
            <>
              {' '}
              <strong>{t('Be careful.')} </strong>
              {t(
                'Changing these settings will affect all charts using this dataset, including charts owned by other people.',
              )}
            </>
          }
        />
        <StyledTableTabs
          id="table-tabs"
          data-test="edit-dataset-tabs"
          onChange={this.handleTabSelect}
          defaultActiveKey={activeTabKey}
          items={[
            {
              key: TABS_KEYS.SOURCE,
              label: t('Source'),
              children: this.renderSourceFieldset(theme),
            },
            {
              key: TABS_KEYS.METRICS,
              label: (
                <CollectionTabTitle
                  collection={sortedMetrics}
                  title={t('Metrics')}
                />
              ),
              children: this.renderMetricCollection(),
            },
            {
              key: TABS_KEYS.COLUMNS,
              label: (
                <CollectionTabTitle
                  collection={this.state.databaseColumns}
                  title={t('Columns')}
                />
              ),
              children: (
                <StyledColumnsTabWrapper>
                  <ColumnButtonWrapper>
                    <StyledButtonWrapper>
                      <Button
                        buttonSize="small"
                        buttonStyle="tertiary"
                        onClick={this.syncMetadata}
                        className="sync-from-source"
                        disabled={this.state.isEditMode}
                      >
                        <Icons.DatabaseOutlined iconSize="m" />
                        {t('Sync columns from source')}
                      </Button>
                    </StyledButtonWrapper>
                  </ColumnButtonWrapper>
                  <ColumnCollectionTable
                    className="columns-table"
                    columns={this.state.databaseColumns}
                    datasource={datasource}
                    onColumnsChange={databaseColumns =>
                      this.setColumns({ databaseColumns })
                    }
                    onDatasourceChange={this.onDatasourceChange}
                  />
                  {this.state.metadataLoading && <Loading />}
                </StyledColumnsTabWrapper>
              ),
            },
            {
              key: TABS_KEYS.CALCULATED_COLUMNS,
              label: (
                <CollectionTabTitle
                  collection={this.state.calculatedColumns}
                  title={t('Calculated columns')}
                />
              ),
              children: (
                <StyledColumnsTabWrapper>
                  <ColumnCollectionTable
                    columns={this.state.calculatedColumns}
                    onColumnsChange={calculatedColumns =>
                      this.setColumns({ calculatedColumns })
                    }
                    columnLabelTooltips={{
                      column_name: t(
                        'This field is used as a unique identifier to attach ' +
                          'the calculated dimension to charts. It is also used ' +
                          'as the alias in the SQL query.',
                      ),
                    }}
                    onDatasourceChange={this.onDatasourceChange}
                    datasource={datasource}
                    editableColumnName
                    showExpression
                    allowAddItem
                    allowEditDataType
                    itemGenerator={() => ({
                      column_name: t('<new column>'),
                      filterable: true,
                      groupby: true,
                      expression: t('<enter SQL expression here>'),
                      expanded: true,
                    })}
                  />
                </StyledColumnsTabWrapper>
              ),
            },
            {
              key: TABS_KEYS.SETTINGS,
              label: t('Settings'),
              children: (
                <Row gutter={16}>
                  <Col xs={24} md={12}>
                    <FormContainer>
                      {this.renderSettingsFieldset()}
                    </FormContainer>
                  </Col>
                  <Col xs={24} md={12}>
                    <FormContainer>
                      {this.renderAdvancedFieldset()}
                    </FormContainer>
                  </Col>
                </Row>
              ),
            },
          ]}
        />
      </DatasourceContainer>
    );
  }

  componentDidMount() {
    Mousetrap.bind('ctrl+shift+f', e => {
      e.preventDefault();
      if (this.state.isEditMode) {
        this.onQueryFormat();
      }
      return false;
    });
  }

  componentWillUnmount() {
    Mousetrap.unbind('ctrl+shift+f');
    this.props.resetQuery();
  }
}

DatasourceEditor.defaultProps = defaultProps;
DatasourceEditor.propTypes = propTypes;

const DataSourceComponent = withTheme(DatasourceEditor);

const mapDispatchToProps = dispatch => ({
  runQuery: payload => dispatch(executeQuery(payload)),
  resetQuery: () => dispatch(resetDatabaseState()),
  formatQuery: sql => dispatch(formatQuery(sql)),
});
const mapStateToProps = state => ({
  database: state?.database,
});
export default withToasts(
  connect(mapStateToProps, mapDispatchToProps)(DataSourceComponent),
);
