import * as path from 'path';
import { mergeTypeDefs } from '@graphql-tools/merge';
import {
  $TSContext,
  AmplifyError,
  exitOnNextTick,
  FeatureFlags,
  pathManager,
  ResourceDoesNotExistError,
  stateManager,
} from '@aws-amplify/amplify-cli-core';
import { printer } from '@aws-amplify/amplify-prompts';
import * as fs from 'fs-extra';
import * as graphql from 'graphql';
import {
  AuroraServerlessMySQLDatabaseReader,
  RelationalDBSchemaTransformer,
  RelationalDBTemplateGenerator,
} from 'graphql-relational-schema-transformer';
import inquirer from 'inquirer';
import _ from 'lodash';
import { getEnvParamManager } from '@aws-amplify/amplify-environment-parameters';
import { supportedDataSources } from '../../provider-utils/supported-datasources';

const subcommand = 'add-graphql-datasource';
const category = 'api';
const providerName = 'awscloudformation';

export const name = subcommand;

/**
 * Entry point for adding RDS data source
 */
export const run = async (context: $TSContext): Promise<void> => {
  try {
    const AWS = await getAwsClient(context, 'list');

    const result: any = await datasourceSelectionPrompt(context, supportedDataSources);

    const providerController = await import(path.join('..', '..', 'provider-utils', result.providerName, 'index'));

    if (!providerController) {
      printer.error('Provider not configured for this category');
      return;
    }

    const { datasource } = result;
    const answers = await providerController.addDatasource(context, category, datasource);

    const { resourceName, databaseName } = answers;

    /**
     * Setting the params using getEnvParamManager.getResourceParamManager.setParams updates
     * the team-provider-info file
     */
    const currentEnv = context.amplify.getEnvInfo().envName;

    getEnvParamManager(currentEnv).getResourceParamManager(category, resourceName).setParams({
      rdsRegion: answers.region,
      rdsClusterIdentifier: answers.dbClusterArn,
      rdsSecretStoreArn: answers.secretStoreArn,
      rdsDatabaseName: answers.databaseName,
    });

    const backendConfig = stateManager.getBackendConfig();

    backendConfig[category][resourceName].rdsInit = true;

    stateManager.setBackendConfig(undefined, backendConfig);

    /**
     * Load the MySqlRelationalDBReader
     */
    const dbReader = new AuroraServerlessMySQLDatabaseReader(
      answers.region,
      answers.secretStoreArn,
      answers.dbClusterArn,
      answers.databaseName,
      AWS,
    );

    /**
     * Instantiate a new Relational Schema Transformer and perform
     * the db introspection to get the GraphQL Schema and Template Context
     */
    // eslint-disable-next-line spellcheck/spell-checker
    const improvePluralizationFlag = FeatureFlags.getBoolean('graphqltransformer.improvePluralization');
    const relationalSchemaTransformer = new RelationalDBSchemaTransformer(dbReader, answers.databaseName, improvePluralizationFlag);
    const graphqlSchemaContext = await relationalSchemaTransformer.introspectDatabaseSchema();

    if (graphqlSchemaContext === null) {
      printer.warn('No importable tables were found in the selected Database.');
      printer.blankLine();
      return;
    }

    /**
     * Merge the GraphQL Schema with the existing schema.graphql in the projects stack
     *
     */
    const apiDirPath = pathManager.getResourceDirectoryPath(undefined, category, resourceName);

    fs.ensureDirSync(apiDirPath);

    const graphqlSchemaFilePath = path.join(apiDirPath, 'schema.graphql');
    const rdsGraphQLSchemaDoc = graphqlSchemaContext.schemaDoc;
    const schemaDirectoryPath = path.join(apiDirPath, 'schema');

    if (fs.existsSync(graphqlSchemaFilePath)) {
      const typesToBeMerged = [rdsGraphQLSchemaDoc];
      const currentGraphQLSchemaDoc = readSchema(graphqlSchemaFilePath);

      if (currentGraphQLSchemaDoc) {
        typesToBeMerged.unshift(currentGraphQLSchemaDoc);
      } else {
        printer.warn(`Graphql Schema file "${graphqlSchemaFilePath}" is empty.`);
        printer.blankLine();
      }

      const concatGraphQLSchemaDoc = mergeTypeDefs(typesToBeMerged);

      fs.writeFileSync(graphqlSchemaFilePath, graphql.print(concatGraphQLSchemaDoc), 'utf8');
    } else if (fs.existsSync(schemaDirectoryPath)) {
      const rdsSchemaFilePath = path.join(schemaDirectoryPath, 'sql.graphql');

      fs.writeFileSync(rdsSchemaFilePath, graphql.print(rdsGraphQLSchemaDoc), 'utf8');
    } else {
      throw new AmplifyError('ApiCategorySchemaNotFoundError', {
        message: 'No schema found',
        resolution: `Your SQL schema should be in either ${graphqlSchemaFilePath} or schema directory ${schemaDirectoryPath}`,
      });
    }

    const resolversDir = path.join(apiDirPath, 'resolvers');

    /**
     * Instantiate a new Relational Template Generator and create
     * the template and relational resolvers
     */

    const templateGenerator = new RelationalDBTemplateGenerator(graphqlSchemaContext);

    let template = templateGenerator.createTemplate(context);

    template = templateGenerator.addRelationalResolvers(template, resolversDir, improvePluralizationFlag);

    const cfn = templateGenerator.printCloudformationTemplate(template);

    /**
     * Add the generated the CFN to the appropriate nested stacks directory
     */

    const stacksDir = path.join(apiDirPath, 'stacks');
    const writeToPath = path.join(stacksDir, `${resourceName}-${databaseName}-rds.json`);

    fs.writeFileSync(writeToPath, cfn, 'utf8');

    await context.amplify.executeProviderUtils(context, 'awscloudformation', 'compileSchema', { forceCompile: true });

    printer.success(`Successfully added the ${datasource} datasource locally`);
    printer.blankLine();
    printer.success('Some next steps:');
    printer.info('"amplify push" will build all your local backend resources and provision it in the cloud');
    printer.info(
      '"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud',
    );
    printer.blankLine();
  } catch (error) {
    printer.error('There was an error adding the datasource');
    throw error;
  }
};

// eslint-disable-next-line @typescript-eslint/no-shadow
const datasourceSelectionPrompt = async (context: $TSContext, supportedDataSources): Promise<unknown> => {
  const options = [];
  Object.keys(supportedDataSources).forEach((datasource) => {
    const optionName =
      supportedDataSources[datasource].alias ||
      `${supportedDataSources[datasource].providerName}:${supportedDataSources[datasource].service}`;
    options.push({
      name: optionName,
      value: {
        provider: supportedDataSources[datasource].provider,
        datasource,
        providerName: supportedDataSources[datasource].provider,
      },
    });
  });

  if (options.length === 0) {
    const errMessage = `No data sources defined by configured providers for category: ${category}`;

    printer.error(errMessage);

    await context.usageData.emitError(new ResourceDoesNotExistError(errMessage));

    exitOnNextTick(1);
  }

  if (options.length === 1) {
    // No need to ask questions
    printer.info(`Using datasource: ${options[0].value.datasource}, provided by: ${options[0].value.providerName}`);

    return new Promise((resolve) => {
      resolve(options[0].value);
    });
  }

  const question = [
    {
      name: 'datasource',
      message: 'Please select from one of the below mentioned data sources',
      type: 'list',
      choices: options,
    },
  ];

  return inquirer.prompt(question).then((answer) => answer.datasource);
};

const getAwsClient = async (context: $TSContext, action: string): Promise<any> => {
  const providerPlugins = context.amplify.getProviderPlugins(context);
  // eslint-disable-next-line
  const provider = require(providerPlugins[providerName]);

  return provider.getConfiguredAWSClient(context, 'aurora-serverless', action);
};

/**
 * Read the GraphQL schema
 */
export const readSchema = (graphqlSchemaFilePath: string): graphql.DocumentNode => {
  const graphqlSchemaRaw = fs.readFileSync(graphqlSchemaFilePath).toString();

  if (graphqlSchemaRaw.trim().length === 0) {
    return null;
  }

  let currentGraphQLSchemaDoc: graphql.DocumentNode;

  try {
    currentGraphQLSchemaDoc = graphql.parse(graphqlSchemaRaw);
  } catch (err) {
    const relativePathToInput = path.relative(process.cwd(), graphqlSchemaRaw);
    throw new AmplifyError('UserInputError', {
      message: `Could not parse graphql scehma \n${relativePathToInput}\n`,
      details: err.message,
      link: 'https://docs.amplify.aws/cli-legacy/graphql-transformer/relational/',
    });
  }

  return currentGraphQLSchemaDoc;
};
