/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import {
  t,
  SupersetTheme,
  FeatureFlag,
  isFeatureEnabled,
} from '@superset-ui/core';
import React, {
  FunctionComponent,
  useEffect,
  useState,
  useReducer,
  Reducer,
} from 'react';
import Tabs from 'src/components/Tabs';
import { Select } from 'src/common/components';
import Alert from 'src/components/Alert';
import Modal from 'src/components/Modal';
import Button from 'src/components/Button';
import IconButton from 'src/components/IconButton';
import InfoTooltip from 'src/components/InfoTooltip';
import withToasts from 'src/messageToasts/enhancers/withToasts';
import {
  testDatabaseConnection,
  useSingleViewResource,
  useAvailableDatabases,
  useDatabaseValidation,
  getDatabaseImages,
  getConnectionAlert,
} from 'src/views/CRUD/hooks';
import { useCommonConf } from 'src/views/CRUD/data/database/state';
import {
  DatabaseObject,
  DatabaseForm,
  CONFIGURATION_METHOD,
  CatalogObject,
} from 'src/views/CRUD/data/database/types';
import Loading from 'src/components/Loading';
import ExtraOptions from './ExtraOptions';
import SqlAlchemyForm from './SqlAlchemyForm';
import DatabaseConnectionForm from './DatabaseConnectionForm';
import {
  antDErrorAlertStyles,
  antDAlertStyles,
  StyledAlertMargin,
  antDModalNoPaddingStyles,
  antDModalStyles,
  antDTabsStyles,
  buttonLinkStyles,
  alchemyButtonLinkStyles,
  TabHeader,
  formHelperStyles,
  formStyles,
  StyledAlignment,
  SelectDatabaseStyles,
  infoTooltip,
  StyledFooterButton,
  StyledStickyHeader,
} from './styles';
import ModalHeader, { DOCUMENTATION_LINK } from './ModalHeader';

const engineSpecificAlertMapping = {
  gsheets: {
    message: 'Why do I need to create a database?',
    description:
      'To begin using your Google Sheets, you need to create a database first. ' +
      'Databases are used as a way to identify ' +
      'your data so that it can be queried and visualized. This ' +
      'database will hold all of your individual Google Sheets ' +
      'you choose to connect here.',
  },
};

const errorAlertMapping = {
  CONNECTION_MISSING_PARAMETERS_ERROR: {
    message: 'Missing Required Fields',
    description: 'Please complete all required fields.',
  },
  CONNECTION_INVALID_HOSTNAME_ERROR: {
    message: 'Could not verify the host',
    description:
      'The host is invalid. Please verify that this field is entered correctly.',
  },
  CONNECTION_PORT_CLOSED_ERROR: {
    message: 'Port is closed',
    description: 'Please verify that port is open to connect.',
  },
  CONNECTION_INVALID_PORT_ERROR: {
    message: 'Invalid Port Number',
    description: 'The port must be a whole number less than or equal to 65535.',
  },
  CONNECTION_ACCESS_DENIED_ERROR: {
    message: 'Invalid account information',
    description: 'Either the username or password is incorrect.',
  },
  CONNECTION_INVALID_PASSWORD_ERROR: {
    message: 'Invalid account information',
    description: 'Either the username or password is incorrect.',
  },
  INVALID_PAYLOAD_SCHEMA_ERROR: {
    message: 'Incorrect Fields',
    description: 'Please make sure all fields are filled out correctly',
  },
  TABLE_DOES_NOT_EXIST_ERROR: {
    message: 'URL could not be identified',
    description:
      'The URL could not be identified. Please check for typos and make sure that "Type of google sheet allowed" selection matches the input',
  },
};
interface DatabaseModalProps {
  addDangerToast: (msg: string) => void;
  addSuccessToast: (msg: string) => void;
  onDatabaseAdd?: (database?: DatabaseObject) => void; // TODO: should we add a separate function for edit?
  onHide: () => void;
  show: boolean;
  databaseId: number | undefined; // If included, will go into edit mode
}

enum ActionType {
  configMethodChange,
  dbSelected,
  editorChange,
  fetched,
  inputChange,
  parametersChange,
  reset,
  textChange,
  extraInputChange,
  extraEditorChange,
  addTableCatalogSheet,
  removeTableCatalogSheet,
}

interface DBReducerPayloadType {
  target?: string;
  name: string;
  json?: {};
  type?: string;
  checked?: boolean;
  value?: string;
}

type DBReducerActionType =
  | {
      type:
        | ActionType.extraEditorChange
        | ActionType.extraInputChange
        | ActionType.textChange
        | ActionType.inputChange
        | ActionType.editorChange
        | ActionType.parametersChange;
      payload: DBReducerPayloadType;
    }
  | {
      type: ActionType.fetched;
      payload: Partial<DatabaseObject>;
    }
  | {
      type: ActionType.dbSelected;
      payload: {
        database_name?: string;
        engine?: string;
        configuration_method: CONFIGURATION_METHOD;
      };
    }
  | {
      type: ActionType.reset | ActionType.addTableCatalogSheet;
    }
  | {
      type: ActionType.removeTableCatalogSheet;
      payload: {
        indexToDelete: number;
      };
    }
  | {
      type: ActionType.configMethodChange;
      payload: {
        database_name?: string;
        engine?: string;
        configuration_method: CONFIGURATION_METHOD;
      };
    };

function dbReducer(
  state: Partial<DatabaseObject> | null,
  action: DBReducerActionType,
): Partial<DatabaseObject> | null {
  const trimmedState = {
    ...(state || {}),
  };
  let query = '';
  let deserializeExtraJSON = {};
  let extra_json: DatabaseObject['extra_json'];

  switch (action.type) {
    case ActionType.extraEditorChange:
      return {
        ...trimmedState,
        extra_json: {
          ...trimmedState.extra_json,
          [action.payload.name]: action.payload.json,
        },
      };
    case ActionType.extraInputChange:
      if (
        action.payload.name === 'schema_cache_timeout' ||
        action.payload.name === 'table_cache_timeout'
      ) {
        return {
          ...trimmedState,
          extra_json: {
            ...trimmedState.extra_json,
            metadata_cache_timeout: {
              ...trimmedState.extra_json?.metadata_cache_timeout,
              [action.payload.name]: action.payload.value,
            },
          },
        };
      }
      if (action.payload.name === 'schemas_allowed_for_csv_upload') {
        return {
          ...trimmedState,
          extra_json: {
            ...trimmedState.extra_json,
            schemas_allowed_for_csv_upload: (action.payload.value || '').split(
              ',',
            ),
          },
        };
      }
      return {
        ...trimmedState,
        extra_json: {
          ...trimmedState.extra_json,
          [action.payload.name]:
            action.payload.type === 'checkbox'
              ? action.payload.checked
              : action.payload.value,
        },
      };
    case ActionType.inputChange:
      if (action.payload.type === 'checkbox') {
        return {
          ...trimmedState,
          [action.payload.name]: action.payload.checked,
        };
      }
      return {
        ...trimmedState,
        [action.payload.name]: action.payload.value,
      };
    case ActionType.parametersChange:
      if (
        trimmedState.catalog !== undefined &&
        action.payload.type?.startsWith('catalog')
      ) {
        // Formatting wrapping google sheets table catalog
        const idx = action.payload.type?.split('-')[1];
        const catalogToUpdate = trimmedState?.catalog[idx] || {};
        catalogToUpdate[action.payload.name] = action.payload.value;

        const paramatersCatalog = {};
        // eslint-disable-next-line array-callback-return
        trimmedState.catalog?.map((item: CatalogObject) => {
          paramatersCatalog[item.name] = item.value;
        });

        return {
          ...trimmedState,
          parameters: {
            ...trimmedState.parameters,
            catalog: paramatersCatalog,
          },
        };
      }
      return {
        ...trimmedState,
        parameters: {
          ...trimmedState.parameters,
          [action.payload.name]: action.payload.value,
        },
      };
    case ActionType.addTableCatalogSheet:
      if (trimmedState.catalog !== undefined) {
        return {
          ...trimmedState,
          catalog: [...trimmedState.catalog, { name: '', value: '' }],
        };
      }
      return {
        ...trimmedState,
        catalog: [{ name: '', value: '' }],
      };
    case ActionType.removeTableCatalogSheet:
      trimmedState.catalog?.splice(action.payload.indexToDelete, 1);
      return {
        ...trimmedState,
      };
    case ActionType.editorChange:
      return {
        ...trimmedState,
        [action.payload.name]: action.payload.json,
      };
    case ActionType.textChange:
      return {
        ...trimmedState,
        [action.payload.name]: action.payload.value,
      };
    case ActionType.fetched:
      // convert all the keys in this payload into strings
      if (action.payload.extra) {
        extra_json = {
          ...JSON.parse(action.payload.extra || ''),
        } as DatabaseObject['extra_json'];

        deserializeExtraJSON = {
          ...JSON.parse(action.payload.extra || ''),
          metadata_params: JSON.stringify(extra_json?.metadata_params),
          engine_params: JSON.stringify(extra_json?.engine_params),
          schemas_allowed_for_csv_upload:
            extra_json?.schemas_allowed_for_csv_upload,
        };
      }

      if (
        action.payload.backend === 'bigquery' &&
        action.payload.configuration_method ===
          CONFIGURATION_METHOD.DYNAMIC_FORM
      ) {
        // convert query into URI params string
        query = new URLSearchParams(
          action?.payload?.parameters?.query as string,
        ).toString();

        return {
          ...action.payload,
          engine: action.payload.backend,
          configuration_method: action.payload.configuration_method,
          extra_json: deserializeExtraJSON,
          parameters: {
            credentials_info: JSON.stringify(
              action.payload?.parameters?.credentials_info || '',
            ),
            query,
          },
        };
      }

      if (
        action.payload.backend === 'gsheets' &&
        action.payload.configuration_method ===
          CONFIGURATION_METHOD.DYNAMIC_FORM &&
        extra_json?.engine_params?.catalog !== undefined
      ) {
        // pull catalog from engine params
        const engineParamsCatalog = extra_json?.engine_params?.catalog;

        return {
          ...action.payload,
          engine: action.payload.backend,
          configuration_method: action.payload.configuration_method,
          extra_json: deserializeExtraJSON,
          catalog: Object.keys(engineParamsCatalog).map(e => ({
            name: e,
            value: engineParamsCatalog[e],
          })),
        } as DatabaseObject;
      }

      if (action.payload?.parameters?.query) {
        // convert query into URI params string
        query = new URLSearchParams(
          action.payload.parameters.query as string,
        ).toString();

        return {
          ...action.payload,
          encrypted_extra: action.payload.encrypted_extra || '',
          engine: action.payload.backend || trimmedState.engine,
          configuration_method: action.payload.configuration_method,
          extra_json: deserializeExtraJSON,
          parameters: {
            ...action.payload.parameters,
            query,
          },
        };
      }

      return {
        ...action.payload,
        encrypted_extra: action.payload.encrypted_extra || '',
        engine: action.payload.backend || trimmedState.engine,
        configuration_method: action.payload.configuration_method,
        extra_json: deserializeExtraJSON,
        parameters: {
          ...action.payload.parameters,
        },
      };

    case ActionType.dbSelected:
      return {
        ...action.payload,
      };
    case ActionType.configMethodChange:
      return {
        ...action.payload,
      };
    case ActionType.reset:
    default:
      return null;
  }
}

const DEFAULT_TAB_KEY = '1';

const serializeExtra = (extraJson: DatabaseObject['extra_json']) =>
  JSON.stringify({
    ...extraJson,
    metadata_params: JSON.parse((extraJson?.metadata_params as string) || '{}'),
    engine_params: JSON.parse(
      ((extraJson?.engine_params as unknown) as string) || '{}',
    ),
    schemas_allowed_for_csv_upload: (
      extraJson?.schemas_allowed_for_csv_upload || []
    ).filter(schema => schema !== ''),
  });

const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
  addDangerToast,
  addSuccessToast,
  onDatabaseAdd,
  onHide,
  show,
  databaseId,
}) => {
  const [db, setDB] = useReducer<
    Reducer<Partial<DatabaseObject> | null, DBReducerActionType>
  >(dbReducer, null);
  const [tabKey, setTabKey] = useState<string>(DEFAULT_TAB_KEY);
  const [availableDbs, getAvailableDbs] = useAvailableDatabases();
  const [
    validationErrors,
    getValidation,
    setValidationErrors,
  ] = useDatabaseValidation();
  const [hasConnectedDb, setHasConnectedDb] = useState<boolean>(false);
  const [dbName, setDbName] = useState('');
  const [editNewDb, setEditNewDb] = useState<boolean>(false);
  const [isLoading, setLoading] = useState<boolean>(false);
  const conf = useCommonConf();
  const dbImages = getDatabaseImages();
  const connectionAlert = getConnectionAlert();
  const isEditMode = !!databaseId;
  const sslForced = isFeatureEnabled(
    FeatureFlag.FORCE_DATABASE_CONNECTIONS_SSL,
  );
  const hasAlert =
    connectionAlert || !!(db?.engine && engineSpecificAlertMapping[db.engine]);
  const useSqlAlchemyForm =
    db?.configuration_method === CONFIGURATION_METHOD.SQLALCHEMY_URI;
  const useTabLayout = isEditMode || useSqlAlchemyForm;
  // Database fetch logic
  const {
    state: { loading: dbLoading, resource: dbFetched, error: dbErrors },
    fetchResource,
    createResource,
    updateResource,
    clearError,
  } = useSingleViewResource<DatabaseObject>(
    'database',
    t('database'),
    addDangerToast,
  );
  const isDynamic = (engine: string | undefined) =>
    availableDbs?.databases.filter(
      (DB: DatabaseObject) => DB.backend === engine || DB.engine === engine,
    )[0].parameters !== undefined;
  const showDBError = validationErrors || dbErrors;
  const isEmpty = (data?: Object | null) =>
    data && Object.keys(data).length === 0;

  const dbModel: DatabaseForm =
    availableDbs?.databases?.find(
      (available: { engine: string | undefined }) =>
        // TODO: we need a centralized engine in one place
        available.engine === (isEditMode ? db?.backend : db?.engine),
    ) || {};

  // Test Connection logic
  const testConnection = () => {
    if (!db?.sqlalchemy_uri) {
      addDangerToast(t('Please enter a SQLAlchemy URI to test'));
      return;
    }

    const connection = {
      sqlalchemy_uri: db?.sqlalchemy_uri || '',
      database_name: db?.database_name?.trim() || undefined,
      impersonate_user: db?.impersonate_user || undefined,
      extra: serializeExtra(db?.extra_json) || undefined,
      encrypted_extra: db?.encrypted_extra || '',
      server_cert: db?.server_cert || undefined,
    };

    testDatabaseConnection(connection, addDangerToast, addSuccessToast);
  };

  const onClose = () => {
    setDB({ type: ActionType.reset });
    setHasConnectedDb(false);
    setValidationErrors(null); // reset validation errors on close
    clearError();
    setEditNewDb(false);
    onHide();
  };

  const onSave = async () => {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { id, ...update } = db || {};

    // Clone DB object
    const dbToUpdate = JSON.parse(JSON.stringify(update));

    if (dbToUpdate.configuration_method === CONFIGURATION_METHOD.DYNAMIC_FORM) {
      if (dbToUpdate?.parameters?.query) {
        // convert query params into dictionary
        const urlParams = new URLSearchParams(dbToUpdate?.parameters?.query);
        dbToUpdate.parameters.query = Object.fromEntries(urlParams);
      } else if (
        dbToUpdate?.parameters?.query === '' &&
        'query' in dbModel.parameters.properties
      ) {
        dbToUpdate.parameters.query = {};
      }

      // Validate DB before saving
      await getValidation(dbToUpdate, true);
      if (validationErrors && !isEmpty(validationErrors)) {
        return;
      }

      const engine = dbToUpdate.backend || dbToUpdate.engine;
      if (engine === 'bigquery' && dbToUpdate.parameters?.credentials_info) {
        // wrap encrypted_extra in credentials_info only for BigQuery
        if (
          dbToUpdate.parameters?.credentials_info &&
          typeof dbToUpdate.parameters?.credentials_info === 'object' &&
          dbToUpdate.parameters?.credentials_info.constructor === Object
        ) {
          // Don't cast if object
          dbToUpdate.encrypted_extra = JSON.stringify({
            credentials_info: dbToUpdate.parameters?.credentials_info,
          });

          // Convert credentials info string before updating
          dbToUpdate.parameters.credentials_info = JSON.stringify(
            dbToUpdate.parameters.credentials_info,
          );
        } else {
          dbToUpdate.encrypted_extra = JSON.stringify({
            credentials_info: JSON.parse(
              dbToUpdate.parameters?.credentials_info,
            ),
          });
        }
      }
    }

    if (dbToUpdate?.parameters?.catalog) {
      // need to stringify gsheets catalog to allow it to be seralized
      dbToUpdate.extra_json = {
        engine_params: JSON.stringify({
          catalog: dbToUpdate.parameters.catalog,
        }),
      };
    }

    if (dbToUpdate?.extra_json) {
      // convert extra_json to back to string
      dbToUpdate.extra = serializeExtra(dbToUpdate?.extra_json);
    }

    if (db?.id) {
      setLoading(true);
      const result = await updateResource(
        db.id as number,
        dbToUpdate as DatabaseObject,
        dbToUpdate.configuration_method === CONFIGURATION_METHOD.DYNAMIC_FORM, // onShow toast on SQLA Forms
      );
      if (result) {
        if (onDatabaseAdd) {
          onDatabaseAdd();
        }
        if (!editNewDb) {
          onClose();
        }
      }
    } else if (db) {
      // Create
      setLoading(true);
      const dbId = await createResource(
        dbToUpdate as DatabaseObject,
        dbToUpdate.configuration_method === CONFIGURATION_METHOD.DYNAMIC_FORM, // onShow toast on SQLA Forms
      );
      if (dbId) {
        setHasConnectedDb(true);
        if (onDatabaseAdd) {
          onDatabaseAdd();
        }
        if (useTabLayout) {
          // tab layout only has one step
          // so it should close immediately on save
          onClose();
        }
      }
    }
    setEditNewDb(false);
    setLoading(false);
  };

  const onChange = (type: any, payload: any) => {
    setDB({ type, payload } as DBReducerActionType);
  };

  // Initialize
  const fetchDB = () => {
    if (isEditMode && databaseId) {
      if (!dbLoading) {
        fetchResource(databaseId).catch(e =>
          addDangerToast(
            t(
              'Sorry there was an error fetching database information: %s',
              e.message,
            ),
          ),
        );
      }
    }
  };

  const setDatabaseModel = (database_name: string) => {
    const selectedDbModel = availableDbs?.databases.filter(
      (db: DatabaseObject) => db.name === database_name,
    )[0];
    const { engine, parameters } = selectedDbModel;
    const isDynamic = parameters !== undefined;
    setDB({
      type: ActionType.dbSelected,
      payload: {
        database_name,
        configuration_method: isDynamic
          ? CONFIGURATION_METHOD.DYNAMIC_FORM
          : CONFIGURATION_METHOD.SQLALCHEMY_URI,
        engine,
      },
    });
    setDB({ type: ActionType.addTableCatalogSheet });
  };

  const renderAvailableSelector = () => (
    <div className="available">
      <h4 className="available-label">
        Or choose from a list of other databases we support:
      </h4>
      <div className="control-label">Supported databases</div>
      <Select
        className="available-select"
        onChange={setDatabaseModel}
        placeholder="Choose a database..."
      >
        {[...(availableDbs?.databases || [])]
          ?.sort((a: DatabaseForm, b: DatabaseForm) =>
            a.name.localeCompare(b.name),
          )
          .map((database: DatabaseForm) => (
            <Select.Option value={database.name} key={database.name}>
              {database.name}
            </Select.Option>
          ))}
      </Select>
      <Alert
        showIcon
        closable={false}
        css={(theme: SupersetTheme) => antDAlertStyles(theme)}
        type="info"
        message={
          connectionAlert?.ADD_DATABASE?.message ||
          t('Want to add a new database?')
        }
        description={
          connectionAlert?.ADD_DATABASE ? (
            <>
              Any databases that allow connections via SQL Alchemy URIs can be
              added.{' '}
              <a
                href={connectionAlert?.ADD_DATABASE.contact_link}
                target="_blank"
                rel="noopener noreferrer"
              >
                {connectionAlert?.ADD_DATABASE.contact_description_link}
              </a>{' '}
              {connectionAlert?.ADD_DATABASE.description}
            </>
          ) : (
            <>
              Any databases that allow connections via SQL Alchemy URIs can be
              added. Learn about how to connect a database driver{' '}
              <a
                href={DOCUMENTATION_LINK}
                target="_blank"
                rel="noopener noreferrer"
              >
                here
              </a>
              .
            </>
          )
        }
      />
    </div>
  );

  const renderPreferredSelector = () => (
    <div className="preferred">
      {availableDbs?.databases
        ?.filter((db: DatabaseForm) => db.preferred)
        .map((database: DatabaseForm) => (
          <IconButton
            className="preferred-item"
            onClick={() => setDatabaseModel(database.name)}
            buttonText={database.name}
            icon={dbImages?.[database.engine]}
          />
        ))}
    </div>
  );

  const handleBackButtonOnFinish = () => {
    if (dbFetched) {
      fetchResource(dbFetched.id as number);
    }
    setEditNewDb(true);
  };

  const handleBackButtonOnConnect = () => {
    if (editNewDb) {
      setHasConnectedDb(false);
    }
    setDB({ type: ActionType.reset });
  };

  const renderModalFooter = () => {
    if (db) {
      // if db show back + connenct
      if (!hasConnectedDb || editNewDb) {
        return (
          <>
            <StyledFooterButton key="back" onClick={handleBackButtonOnConnect}>
              Back
            </StyledFooterButton>
            <StyledFooterButton
              key="submit"
              buttonStyle="primary"
              onClick={onSave}
            >
              Connect
            </StyledFooterButton>
          </>
        );
      }

      return (
        <>
          <StyledFooterButton key="back" onClick={handleBackButtonOnFinish}>
            Back
          </StyledFooterButton>
          <StyledFooterButton
            key="submit"
            buttonStyle="primary"
            onClick={onSave}
            data-test="modal-confirm-button"
          >
            Finish
          </StyledFooterButton>
        </>
      );
    }
    return [];
  };

  const renderEditModalFooter = () => (
    <>
      <StyledFooterButton key="close" onClick={onClose}>
        Close
      </StyledFooterButton>
      <StyledFooterButton key="submit" buttonStyle="primary" onClick={onSave}>
        Finish
      </StyledFooterButton>
    </>
  );
  useEffect(() => {
    if (show) {
      setTabKey(DEFAULT_TAB_KEY);
      getAvailableDbs();
      setLoading(true);
    }
    if (databaseId && show) {
      fetchDB();
    }
  }, [show, databaseId]);

  useEffect(() => {
    if (dbFetched) {
      setDB({
        type: ActionType.fetched,
        payload: dbFetched,
      });
      // keep a copy of the name separate for display purposes
      // because it shouldn't change when the form is updated
      setDbName(dbFetched.database_name);
    }
  }, [dbFetched]);

  useEffect(() => {
    if (isLoading) {
      setLoading(false);
    }
  }, [availableDbs]);

  const tabChange = (key: string) => {
    setTabKey(key);
  };

  const renderStepTwoAlert = () =>
    db?.engine && (
      <StyledAlertMargin>
        <Alert
          closable={false}
          css={(theme: SupersetTheme) => antDAlertStyles(theme)}
          type="info"
          showIcon
          message={
            engineSpecificAlertMapping[db.engine]?.message ||
            connectionAlert?.DEFAULT?.message
          }
          description={
            engineSpecificAlertMapping[db.engine]?.description ||
            connectionAlert?.DEFAULT?.description
          }
        />
      </StyledAlertMargin>
    );

  const errorAlert = () => {
    if (
      isEmpty(dbErrors) ||
      (isEmpty(validationErrors) &&
        !(validationErrors?.error_type in errorAlertMapping))
    ) {
      return <></>;
    }

    if (validationErrors) {
      return (
        <Alert
          type="error"
          css={(theme: SupersetTheme) => antDErrorAlertStyles(theme)}
          message={
            errorAlertMapping[validationErrors?.error_type]?.message ||
            validationErrors?.error_type
          }
          description={
            errorAlertMapping[validationErrors?.error_type]?.description ||
            JSON.stringify(validationErrors)
          }
          showIcon
          closable={false}
        />
      );
    }

    const message: Array<string> = Object.values(dbErrors);
    return (
      <Alert
        type="error"
        css={(theme: SupersetTheme) => antDErrorAlertStyles(theme)}
        message="Database Creation Error"
        description={message[0]}
      />
    );
  };

  const renderFinishState = () => {
    if (!editNewDb) {
      return (
        <ExtraOptions
          db={db as DatabaseObject}
          onInputChange={({ target }: { target: HTMLInputElement }) =>
            onChange(ActionType.inputChange, {
              type: target.type,
              name: target.name,
              checked: target.checked,
              value: target.value,
            })
          }
          onTextChange={({ target }: { target: HTMLTextAreaElement }) =>
            onChange(ActionType.textChange, {
              name: target.name,
              value: target.value,
            })
          }
          onEditorChange={(payload: { name: string; json: any }) =>
            onChange(ActionType.editorChange, payload)
          }
          onExtraInputChange={({ target }: { target: HTMLInputElement }) => {
            onChange(ActionType.extraInputChange, {
              type: target.type,
              name: target.name,
              checked: target.checked,
              value: target.value,
            });
          }}
          onExtraEditorChange={(payload: { name: string; json: any }) =>
            onChange(ActionType.extraEditorChange, payload)
          }
        />
      );
    }
    return (
      <DatabaseConnectionForm
        isEditMode
        sslForced={sslForced}
        dbModel={dbModel}
        db={db as DatabaseObject}
        onParametersChange={({ target }: { target: HTMLInputElement }) =>
          onChange(ActionType.parametersChange, {
            type: target.type,
            name: target.name,
            checked: target.checked,
            value: target.value,
          })
        }
        onChange={({ target }: { target: HTMLInputElement }) =>
          onChange(ActionType.textChange, {
            name: target.name,
            value: target.value,
          })
        }
        onAddTableCatalog={() =>
          setDB({ type: ActionType.addTableCatalogSheet })
        }
        onRemoveTableCatalog={(idx: number) =>
          setDB({
            type: ActionType.removeTableCatalogSheet,
            payload: { indexToDelete: idx },
          })
        }
        getValidation={() => getValidation(db)}
        validationErrors={validationErrors}
      />
    );
  };

  return useTabLayout ? (
    <Modal
      css={(theme: SupersetTheme) => [
        antDTabsStyles,
        antDModalNoPaddingStyles,
        antDModalStyles(theme),
        formHelperStyles(theme),
        formStyles(theme),
      ]}
      name="database"
      data-test="database-modal"
      onHandledPrimaryAction={onSave}
      onHide={onClose}
      primaryButtonName={isEditMode ? t('Save') : t('Connect')}
      width="500px"
      centered
      show={show}
      title={
        <h4>{isEditMode ? t('Edit database') : t('Connect a database')}</h4>
      }
      footer={isEditMode ? renderEditModalFooter() : renderModalFooter()}
    >
      <StyledStickyHeader>
        <TabHeader>
          <ModalHeader
            isLoading={isLoading}
            isEditMode={isEditMode}
            useSqlAlchemyForm={useSqlAlchemyForm}
            hasConnectedDb={hasConnectedDb}
            db={db}
            dbName={dbName}
            dbModel={dbModel}
          />
        </TabHeader>
      </StyledStickyHeader>
      <Tabs
        defaultActiveKey={DEFAULT_TAB_KEY}
        activeKey={tabKey}
        onTabClick={tabChange}
        animated={{ inkBar: true, tabPane: true }}
      >
        <Tabs.TabPane tab={<span>{t('Basic')}</span>} key="1">
          {useSqlAlchemyForm ? (
            <StyledAlignment>
              <SqlAlchemyForm
                db={db as DatabaseObject}
                onInputChange={({ target }: { target: HTMLInputElement }) =>
                  onChange(ActionType.inputChange, {
                    type: target.type,
                    name: target.name,
                    checked: target.checked,
                    value: target.value,
                  })
                }
                conf={conf}
                testConnection={testConnection}
                isEditMode={isEditMode}
              />
              {isDynamic(db?.backend || db?.engine) && !isEditMode && (
                <div css={(theme: SupersetTheme) => infoTooltip(theme)}>
                  <Button
                    buttonStyle="link"
                    onClick={() =>
                      setDB({
                        type: ActionType.configMethodChange,
                        payload: {
                          database_name: db?.database_name,
                          configuration_method:
                            CONFIGURATION_METHOD.DYNAMIC_FORM,
                          engine: db?.engine,
                        },
                      })
                    }
                    css={theme => alchemyButtonLinkStyles(theme)}
                  >
                    Connect this database using the dynamic form instead
                  </Button>
                  <InfoTooltip
                    tooltip={t(
                      'Click this link to switch to an alternate form that exposes only the required fields needed to connect this database.',
                    )}
                    viewBox="0 -6 24 24"
                  />
                </div>
              )}
            </StyledAlignment>
          ) : (
            <DatabaseConnectionForm
              isEditMode
              sslForced={sslForced}
              dbModel={dbModel}
              db={db as DatabaseObject}
              onParametersChange={({ target }: { target: HTMLInputElement }) =>
                onChange(ActionType.parametersChange, {
                  type: target.type,
                  name: target.name,
                  checked: target.checked,
                  value: target.value,
                })
              }
              onChange={({ target }: { target: HTMLInputElement }) =>
                onChange(ActionType.textChange, {
                  name: target.name,
                  value: target.value,
                })
              }
              onAddTableCatalog={() =>
                setDB({ type: ActionType.addTableCatalogSheet })
              }
              onRemoveTableCatalog={(idx: number) =>
                setDB({
                  type: ActionType.removeTableCatalogSheet,
                  payload: { indexToDelete: idx },
                })
              }
              getValidation={() => getValidation(db)}
              validationErrors={validationErrors}
            />
          )}
          {!isEditMode && (
            <StyledAlertMargin>
              <Alert
                closable={false}
                css={(theme: SupersetTheme) => antDAlertStyles(theme)}
                message="Additional fields may be required"
                showIcon
                description={
                  <>
                    Select databases require additional fields to be completed
                    in the Advanced tab to successfully connect the database.
                    Learn what requirements your databases has{' '}
                    <a
                      href={DOCUMENTATION_LINK}
                      target="_blank"
                      rel="noopener noreferrer"
                      className="additional-fields-alert-description"
                    >
                      here
                    </a>
                    .
                  </>
                }
                type="info"
              />
            </StyledAlertMargin>
          )}
        </Tabs.TabPane>
        <Tabs.TabPane tab={<span>{t('Advanced')}</span>} key="2">
          <ExtraOptions
            db={db as DatabaseObject}
            onInputChange={({ target }: { target: HTMLInputElement }) =>
              onChange(ActionType.inputChange, {
                type: target.type,
                name: target.name,
                checked: target.checked,
                value: target.value,
              })
            }
            onTextChange={({ target }: { target: HTMLTextAreaElement }) =>
              onChange(ActionType.textChange, {
                name: target.name,
                value: target.value,
              })
            }
            onEditorChange={(payload: { name: string; json: any }) =>
              onChange(ActionType.editorChange, payload)
            }
            onExtraInputChange={({ target }: { target: HTMLInputElement }) => {
              onChange(ActionType.extraInputChange, {
                type: target.type,
                name: target.name,
                checked: target.checked,
                value: target.value,
              });
            }}
            onExtraEditorChange={(payload: { name: string; json: any }) => {
              onChange(ActionType.extraEditorChange, payload);
            }}
          />
          {showDBError && errorAlert()}
        </Tabs.TabPane>
      </Tabs>
    </Modal>
  ) : (
    <Modal
      css={(theme: SupersetTheme) => [
        antDModalNoPaddingStyles,
        antDModalStyles(theme),
        formHelperStyles(theme),
        formStyles(theme),
      ]}
      name="database"
      onHandledPrimaryAction={onSave}
      onHide={onClose}
      primaryButtonName={hasConnectedDb ? t('Finish') : t('Connect')}
      width="500px"
      centered
      show={show}
      title={<h4>{t('Connect a database')}</h4>}
      footer={renderModalFooter()}
    >
      {hasConnectedDb ? (
        <>
          <ModalHeader
            isLoading={isLoading}
            isEditMode={isEditMode}
            useSqlAlchemyForm={useSqlAlchemyForm}
            hasConnectedDb={hasConnectedDb}
            db={db}
            dbName={dbName}
            dbModel={dbModel}
            editNewDb={editNewDb}
          />
          {renderFinishState()}
        </>
      ) : (
        <>
          {/* Dyanmic Form Step 1 */}
          {!isLoading &&
            (!db ? (
              <SelectDatabaseStyles>
                <ModalHeader
                  isLoading={isLoading}
                  isEditMode={isEditMode}
                  useSqlAlchemyForm={useSqlAlchemyForm}
                  hasConnectedDb={hasConnectedDb}
                  db={db}
                  dbName={dbName}
                  dbModel={dbModel}
                />
                {renderPreferredSelector()}
                {renderAvailableSelector()}
              </SelectDatabaseStyles>
            ) : (
              <>
                <ModalHeader
                  isLoading={isLoading}
                  isEditMode={isEditMode}
                  useSqlAlchemyForm={useSqlAlchemyForm}
                  hasConnectedDb={hasConnectedDb}
                  db={db}
                  dbName={dbName}
                  dbModel={dbModel}
                />
                {hasAlert && renderStepTwoAlert()}
                <DatabaseConnectionForm
                  db={db}
                  sslForced={sslForced}
                  dbModel={dbModel}
                  onAddTableCatalog={() => {
                    setDB({ type: ActionType.addTableCatalogSheet });
                  }}
                  onRemoveTableCatalog={(idx: number) => {
                    setDB({
                      type: ActionType.removeTableCatalogSheet,
                      payload: { indexToDelete: idx },
                    });
                  }}
                  onParametersChange={({
                    target,
                  }: {
                    target: HTMLInputElement;
                  }) =>
                    onChange(ActionType.parametersChange, {
                      type: target.type,
                      name: target.name,
                      checked: target.checked,
                      value: target.value,
                    })
                  }
                  onChange={({ target }: { target: HTMLInputElement }) =>
                    onChange(ActionType.textChange, {
                      name: target.name,
                      value: target.value,
                    })
                  }
                  getValidation={() => getValidation(db)}
                  validationErrors={validationErrors}
                />
                <div css={(theme: SupersetTheme) => infoTooltip(theme)}>
                  <Button
                    data-test="sqla-connect-btn"
                    buttonStyle="link"
                    onClick={() =>
                      setDB({
                        type: ActionType.configMethodChange,
                        payload: {
                          engine: db.engine,
                          configuration_method:
                            CONFIGURATION_METHOD.SQLALCHEMY_URI,
                          database_name: db.database_name,
                        },
                      })
                    }
                    css={buttonLinkStyles}
                  >
                    Connect this database with a SQLAlchemy URI string instead
                  </Button>
                  <InfoTooltip
                    tooltip={t(
                      'Click this link to switch to an alternate form that allows you to input the SQLAlchemy URL for this database manually.',
                    )}
                    viewBox="0 -6 24 24"
                  />
                </div>
                {/* Step 2 */}
                {showDBError && errorAlert()}
              </>
            ))}
        </>
      )}
      {isLoading && <Loading />}
    </Modal>
  );
};

export default withToasts(DatabaseModal);
