/**
 * 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 { useCallback, useState, FormEvent } from 'react';
import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
import { Radio, RadioChangeEvent } from '@superset-ui/core/components/Radio';
import {
  AsyncSelect,
  Button,
  Checkbox,
  Modal,
  Input,
  type SelectValue,
  Icons,
  Flex,
} from '@superset-ui/core/components';
import { t } from '@apache-superset/core/translation';
import {
  SupersetClient,
  JsonResponse,
  JsonObject,
  QueryResponse,
  QueryFormData,
  VizType,
  FeatureFlag,
  isFeatureEnabled,
  getClientErrorObject,
} from '@superset-ui/core';
import { styled } from '@apache-superset/core/theme';
import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
import { useSelector, useDispatch } from 'react-redux';
import rison from 'rison';
import { createDatasource } from 'src/SqlLab/actions/sqlLab';
import { addDangerToast } from 'src/components/MessageToasts/actions';
import { UserWithPermissionsAndRoles as User } from 'src/types/bootstrapTypes';
import {
  DatasetRadioState,
  EXPLORE_CHART_DEFAULT,
  DatasetOwner,
  SqlLabRootState,
} from 'src/SqlLab/types';
import { mountExploreUrl } from 'src/explore/exploreUtils';
import { postFormData } from 'src/explore/exploreUtils/formData';
import { URL_PARAMS } from 'src/constants';
import { isEmpty } from 'lodash';
import { clearDatasetCache } from 'src/utils/cachedSupersetGet';

interface QueryDatabase {
  id?: number;
}

export type ExploreQuery = QueryResponse & {
  database?: QueryDatabase | null | undefined;
};

export interface ISimpleColumn {
  column_name?: string | null;
  name?: string | null;
  type?: string | null;
  is_dttm?: boolean | null;
}

export type Database = {
  backend: string;
  id: number;
  parameter: object;
};

export interface ISaveableDatasource {
  columns: ISimpleColumn[];
  name: string;
  dbId: number;
  sql: string;
  templateParams?: string | object | null;
  catalog?: string | null;
  schema?: string | null;
  database?: Database;
}

interface SaveDatasetModalProps {
  visible: boolean;
  onHide: () => void;
  buttonTextOnSave: string;
  buttonTextOnOverwrite: string;
  modalDescription?: string;
  datasource: ISaveableDatasource;
  openWindow?: boolean;
  formData?: Omit<QueryFormData, 'datasource'>;
}

const Styles = styled.div`
  ${({ theme }) => `
    .sdm-body {
      padding: ${theme.sizeUnit * 4}px ${theme.sizeUnit * 6}px;
    }

    .sdm-prompt {
      margin-bottom: ${theme.sizeUnit * 4}px;
      color: ${theme.colorTextSecondary};
    }

    .sdm-radio-group {
      display: flex;
      flex-direction: column;
      gap: ${theme.sizeUnit * 6}px;
    }

    .sdm-radio-option {
      display: flex;
      flex-direction: column;
      gap: ${theme.sizeUnit * 3}px;
    }

    .sdm-radio {
      margin: 0;
      .ant-radio {
        margin-right: ${theme.sizeUnit * 2}px;
      }
      .ant-radio-wrapper {
        color: ${theme.colorText};
      }
    }

    .sdm-form-field {
      margin-left: 0;
      max-width: 400px;
    }

    .sdm-overwrite-msg {
      padding: ${theme.sizeUnit * 4}px ${theme.sizeUnit * 6}px;
      text-align: center;
      color: ${theme.colorText};
    }
  `}
`;
type UpdateDatasetPayload = {
  dbId: number;
  datasetId: number;
  sql: string;
  columns: Array<Record<string, any>>;
  owners: number[];
  overrideColumns: boolean;
  templateParams?: string;
};

const updateDataset = async ({
  dbId,
  datasetId,
  sql,
  columns,
  owners,
  overrideColumns,
  templateParams,
}: UpdateDatasetPayload) => {
  const endpoint = `api/v1/dataset/${datasetId}?override_columns=${overrideColumns}`;
  const headers = { 'Content-Type': 'application/json' };
  const body = JSON.stringify({
    sql,
    columns,
    owners,
    database_id: dbId,
    ...(templateParams !== undefined && { template_params: templateParams }),
  });

  const data: JsonResponse = await SupersetClient.put({
    endpoint,
    headers,
    body,
  });

  clearDatasetCache(datasetId);

  return data.json.result;
};

const UNTITLED = t('Untitled Dataset');

// The filters param is only used to test jinja templates.
// Remove the special filters entry from the templateParams
// before saving the dataset.
const sanitizeTemplateParams = (
  templateParams: string | object | null | undefined,
): string | undefined => {
  if (typeof templateParams !== 'string') {
    return undefined;
  }
  try {
    const parsed = JSON.parse(templateParams) as Record<string, unknown>;
    // Remove the special _filters entry — it is only used to test jinja templates.
    const { _filters: _ignored, ...clean } = parsed;
    return JSON.stringify(clean);
  } catch (e) {
    // malformed templateParams, do not include it
    return undefined;
  }
};

export const SaveDatasetModal = ({
  visible,
  onHide,
  buttonTextOnSave,
  buttonTextOnOverwrite,
  modalDescription,
  datasource,
  openWindow = true,
  formData = {},
}: SaveDatasetModalProps) => {
  const defaultVizType = useSelector<SqlLabRootState, string>(
    state => state.common?.conf?.DEFAULT_VIZ_TYPE || VizType.Table,
  );

  const getDefaultDatasetName = () =>
    `${datasource?.name || UNTITLED} ${dayjs().format('L HH:mm:ss')}`;
  const [datasetName, setDatasetName] = useState(getDefaultDatasetName());
  const [newOrOverwrite, setNewOrOverwrite] = useState(
    DatasetRadioState.SaveNew,
  );
  const [shouldOverwriteDataset, setShouldOverwriteDataset] = useState(false);
  const [datasetToOverwrite, setDatasetToOverwrite] = useState<
    Record<string, any>
  >({});
  const [selectedDatasetToOverwrite, setSelectedDatasetToOverwrite] = useState<
    SelectValue | undefined
  >(undefined);
  const [loading, setLoading] = useState<boolean>(false);

  const user = useSelector<SqlLabRootState, User>(state => state.user);
  const dispatch = useDispatch<(dispatch: any) => Promise<JsonObject>>();
  const [includeTemplateParameters, setIncludeTemplateParameters] =
    useState(false);

  const createWindow = (url: string) => {
    if (openWindow) {
      window.open(url, '_blank', 'noreferrer');
    } else {
      window.location.href = url;
    }
  };
  const formDataWithDefaults = {
    ...EXPLORE_CHART_DEFAULT,
    ...formData,
  };
  const handleOverwriteDataset = async () => {
    // if user wants to overwrite a dataset we need to prompt them
    if (!shouldOverwriteDataset) {
      setShouldOverwriteDataset(true);
      return;
    }
    setLoading(true);

    const templateParams = includeTemplateParameters
      ? sanitizeTemplateParams(datasource?.templateParams)
      : undefined;

    try {
      const [, key] = await Promise.all([
        updateDataset({
          dbId: datasource?.dbId,
          datasetId: datasetToOverwrite?.datasetid,
          sql: datasource?.sql,
          columns: datasource?.columns?.map(
            (d: { column_name: string; type: string; is_dttm: boolean }) => ({
              column_name: d.column_name,
              type: d.type,
              is_dttm: d.is_dttm,
            }),
          ),
          owners: datasetToOverwrite?.owners?.map((o: DatasetOwner) => o.id),
          overrideColumns: true,
          templateParams,
        }),
        postFormData(datasetToOverwrite.datasetid, 'table', {
          ...formDataWithDefaults,
          datasource: `${datasetToOverwrite.datasetid}__table`,
          ...(defaultVizType === VizType.Table && {
            all_columns: datasource?.columns?.map(column => column.column_name),
          }),
        }),
      ]);
      setLoading(false);

      const url = mountExploreUrl('base', {
        [URL_PARAMS.formDataKey.name]: key,
      });
      createWindow(url);

      setShouldOverwriteDataset(false);
      setDatasetName(getDefaultDatasetName());
      onHide();
    } catch (error) {
      setLoading(false);
      getClientErrorObject(error).then((e: { error: string }) => {
        dispatch(
          addDangerToast(
            e.error || t('An error occurred while overwriting the dataset'),
          ),
        );
      });
    }
  };

  const loadDatasetOverwriteOptions = useCallback(
    async (input = '') => {
      const { userId } = user;
      const queryParams = rison.encode({
        filters: [
          {
            col: 'table_name',
            opr: 'ct',
            value: input,
          },
          {
            col: 'owners',
            opr: 'rel_m_m',
            value: userId,
          },
        ],
        order_column: 'changed_on_delta_humanized',
        order_direction: 'desc',
      });

      return SupersetClient.get({
        endpoint: `/api/v1/dataset/?q=${queryParams}`,
      }).then(response => ({
        data: response.json.result.map(
          (r: { table_name: string; id: number; owners: [DatasetOwner] }) => ({
            value: r.table_name,
            label: r.table_name,
            datasetid: r.id,
            owners: r.owners,
          }),
        ),
        totalCount: response.json.count,
      }));
    },
    [user],
  );

  const handleSaveInDataset = () => {
    setLoading(true);
    const selectedColumns = datasource?.columns ?? [];

    const templateParams = includeTemplateParameters
      ? sanitizeTemplateParams(datasource?.templateParams)
      : undefined;

    dispatch(
      createDatasource({
        sql: datasource.sql,
        dbId: (datasource.dbId ?? datasource?.database?.id) as number,
        catalog: datasource?.catalog ?? null,
        schema: datasource?.schema ?? '',
        templateParams,
        datasourceName: datasetName,
      }),
    )
      .then((data: { id: number }) => {
        clearDatasetCache(data.id);

        return postFormData(data.id, 'table', {
          ...formDataWithDefaults,
          datasource: `${data.id}__table`,
          ...(defaultVizType === VizType.Table && {
            all_columns: selectedColumns.map(column => column.column_name),
          }),
        });
      })
      .then((key: string) => {
        setLoading(false);
        const url = mountExploreUrl('base', {
          [URL_PARAMS.formDataKey.name]: key,
        });
        createWindow(url);
        setDatasetName(getDefaultDatasetName());
        onHide();
      })
      .catch(() => {
        setLoading(false);
        addDangerToast(t('An error occurred saving dataset'));
      });
  };

  const handleOverwriteDatasetOption = (value: SelectValue, option: any) => {
    setDatasetToOverwrite(option);
    setSelectedDatasetToOverwrite(value);
  };

  const handleDatasetNameChange = (e: FormEvent<HTMLInputElement>) => {
    // @ts-expect-error
    setDatasetName(e.target.value);
  };

  const handleOverwriteCancel = () => {
    setShouldOverwriteDataset(false);
    setDatasetToOverwrite({});
  };

  const disableSaveAndExploreBtn =
    (newOrOverwrite === DatasetRadioState.SaveNew &&
      datasetName.length === 0) ||
    (newOrOverwrite === DatasetRadioState.OverwriteDataset &&
      isEmpty(selectedDatasetToOverwrite));

  const filterAutocompleteOption = (
    inputValue: string,
    option: { value: string; datasetid: number },
  ) => option.value.toLowerCase().includes(inputValue.toLowerCase());

  return (
    <Modal
      show={visible}
      name={t('Save or Overwrite Dataset')}
      title={
        <ModalTitleWithIcon
          title={t('Save or Overwrite Dataset')}
          icon={<Icons.SaveOutlined />}
          data-test="save-or-overwrite-dataset-title"
        />
      }
      onHide={onHide}
      footer={
        <Flex align="center" justify="flex-end" gap="8px">
          {isFeatureEnabled(FeatureFlag.EnableTemplateProcessing) && (
            <div style={{ display: 'flex', alignItems: 'center' }}>
              <Checkbox
                checked={includeTemplateParameters}
                onChange={e =>
                  setIncludeTemplateParameters(e.target.checked ?? false)
                }
              />
              <span style={{ marginLeft: '5px' }}>
                {t('Include Template Parameters')}
              </span>
            </div>
          )}
          {newOrOverwrite === DatasetRadioState.SaveNew && (
            <Button
              disabled={disableSaveAndExploreBtn}
              buttonStyle="primary"
              onClick={handleSaveInDataset}
              loading={loading}
            >
              {buttonTextOnSave}
            </Button>
          )}
          {newOrOverwrite === DatasetRadioState.OverwriteDataset && (
            <>
              {shouldOverwriteDataset && (
                <Button onClick={handleOverwriteCancel}>{t('Back')}</Button>
              )}
              <Button
                className="md"
                buttonStyle="primary"
                onClick={handleOverwriteDataset}
                disabled={disableSaveAndExploreBtn}
                loading={loading}
              >
                {buttonTextOnOverwrite}
              </Button>
            </>
          )}
        </Flex>
      }
    >
      <Styles>
        {!shouldOverwriteDataset && (
          <div className="sdm-body">
            {modalDescription && (
              <div className="sdm-prompt">{modalDescription}</div>
            )}
            <Radio.Group
              onChange={(e: RadioChangeEvent) => {
                setNewOrOverwrite(Number(e.target.value));
              }}
              value={newOrOverwrite}
              className="sdm-radio-group"
            >
              <div className="sdm-radio-option">
                <Radio className="sdm-radio" value={1}>
                  {t('Save as new')}
                </Radio>
                <div className="sdm-form-field">
                  <Input
                    value={datasetName}
                    onChange={handleDatasetNameChange}
                    disabled={newOrOverwrite !== 1}
                    placeholder={t('Dataset name')}
                  />
                </div>
              </div>

              <div className="sdm-radio-option">
                <Radio className="sdm-radio" value={2}>
                  {t('Overwrite existing')}
                </Radio>
                <div className="sdm-form-field">
                  <AsyncSelect
                    allowClear
                    showSearch
                    placeholder={t('Select or type dataset name')}
                    ariaLabel={t('Existing dataset')}
                    onChange={handleOverwriteDatasetOption}
                    options={input => loadDatasetOverwriteOptions(input)}
                    value={selectedDatasetToOverwrite}
                    filterOption={filterAutocompleteOption}
                    disabled={newOrOverwrite !== 2}
                    getPopupContainer={() => document.body}
                  />
                </div>
              </div>
            </Radio.Group>
          </div>
        )}
        {shouldOverwriteDataset && (
          <div className="sdm-overwrite-msg">
            {t('Are you sure you want to overwrite this dataset?')}
          </div>
        )}
      </Styles>
    </Modal>
  );
};
