import * as path from 'path';
import { Duration, Expiration } from 'aws-cdk-lib';
import {
  $TSContext,
  exitOnNextTick,
  FeatureFlags,
  open,
  pathManager,
  ResourceAlreadyExistsError,
  ResourceDoesNotExistError,
  stateManager,
  UnknownResourceTypeError,
  getGraphQLTransformerAuthDocLink,
  ApiCategoryFacade,
} from '@aws-amplify/amplify-cli-core';
import { UpdateApiRequest } from 'amplify-headless-interface';
import { printer, prompter } from '@aws-amplify/amplify-prompts';
import chalk from 'chalk';
import * as fs from 'fs-extra';
import { collectDirectivesByTypeNames, readProjectConfiguration } from 'graphql-transformer-core';
import inquirer, { CheckboxQuestion, ListChoiceOptions, ListQuestion } from 'inquirer';
import _ from 'lodash';
import { v4 as uuid } from 'uuid';
import { category } from '../../../category-constants';
import { rootAssetDir } from '../aws-constants';
import { getAllDefaults } from '../default-values/appSync-defaults';
import { dataStoreLearnMore } from '../sync-conflict-handler-assets/syncAssets';
import { authConfigHasApiKey, checkIfAuthExists, getAppSyncAuthConfig } from '../utils/amplify-meta-utils';
import { authConfigToAppSyncAuthType } from '../utils/auth-config-to-app-sync-auth-type-bi-di-mapper';
import { checkAppsyncApiResourceMigration } from '../utils/check-appsync-api-migration';
import { defineGlobalSandboxMode } from '../utils/global-sandbox-mode';
import { resolverConfigToConflictResolution } from '../utils/resolver-config-to-conflict-resolution-bi-di-mapper';

const serviceName = 'AppSync';
const elasticContainerServiceName = 'ElasticContainer';
const providerName = 'awscloudformation';
const graphqlSchemaDir = path.join(rootAssetDir, 'graphql-schemas');

// keep in sync with ServiceName in amplify-category-function, but probably it will not change
const FunctionServiceNameLambdaFunction = 'Lambda';

const authProviderChoices = [
  {
    name: 'API key',
    value: 'API_KEY',
  },
  {
    name: 'Amazon Cognito User Pool',
    value: 'AMAZON_COGNITO_USER_POOLS',
  },
  {
    name: 'IAM',
    value: 'AWS_IAM',
  },
  {
    name: 'OpenID Connect',
    value: 'OPENID_CONNECT',
  },
];

const conflictResolutionHanlderChoices = [
  {
    name: 'Auto Merge',
    value: 'AUTOMERGE',
  },
  {
    name: 'Optimistic Concurrency',
    value: 'OPTIMISTIC_CONCURRENCY',
  },
  {
    name: 'Custom Lambda',
    value: 'LAMBDA',
  },
  {
    name: 'Learn More',
    value: 'Learn More',
  },
];

const blankSchemaFile = 'blank-schema.graphql';
const schemaTemplatesV1 = [
  {
    name: 'Single object with fields (e.g., “Todo” with ID, name, description)',
    value: 'single-object-schema.graphql',
  },
  {
    name: 'One-to-many relationship (e.g., “Blogs” with “Posts” and “Comments”)',
    value: 'many-relationship-schema.graphql',
  },
  {
    name: 'Objects with fine-grained access control (e.g., a project management app with owner-based authorization)',
    value: 'single-object-auth-schema.graphql',
  },
  {
    name: 'Blank Schema',
    value: blankSchemaFile,
  },
];

const schemaTemplatesV2 = [
  {
    name: 'Single object with fields (e.g., “Todo” with ID, name, description)',
    value: 'single-object-schema-v2.graphql',
  },
  {
    name: 'One-to-many relationship (e.g., “Blogs” with “Posts” and “Comments”)',
    value: 'many-relationship-schema-v2.graphql',
  },
  {
    name: 'Objects with fine-grained access control (e.g., a project management app with owner-based authorization)',
    value: 'single-object-auth-schema-v2.graphql',
  },
  {
    name: 'Blank Schema',
    value: blankSchemaFile,
  },
];

export const openConsole = async (context: $TSContext) => {
  const amplifyMeta = stateManager.getMeta();
  const categoryAmplifyMeta = amplifyMeta[category];
  const { Region } = amplifyMeta.providers[providerName];

  const graphQLApis = Object.keys(categoryAmplifyMeta).filter((resourceName) => {
    const resource = categoryAmplifyMeta[resourceName];

    return (
      resource.output &&
      (resource.service === serviceName || (resource.service === elasticContainerServiceName && resource.apiType === 'GRAPHQL'))
    );
  });

  if (graphQLApis) {
    let url;
    let selectedApi = graphQLApis[0];

    if (graphQLApis.length > 1) {
      ({ selectedApi } = await inquirer.prompt({
        type: 'list',
        name: 'selectedApi',
        choices: graphQLApis,
        message: 'Please select the API',
      }));
    }

    const selectedResource = categoryAmplifyMeta[selectedApi];

    if (selectedResource.service === serviceName) {
      const {
        output: { GraphQLAPIIdOutput },
      } = selectedResource;
      const appId = amplifyMeta.providers[providerName].AmplifyAppId;
      if (!appId) {
        throw new Error('Missing AmplifyAppId in amplify-meta.json');
      }

      url = `https://console.aws.amazon.com/appsync/home?region=${Region}#/${GraphQLAPIIdOutput}/v1/queries`;

      const providerPlugin = await import(context.amplify.getProviderPlugins(context)[providerName]);
      const { isAdminApp, region } = await providerPlugin.isAmplifyAdminApp(appId);
      if (isAdminApp) {
        if (region !== Region) {
          printer.warn(`Region mismatch: Amplify service returned '${region}', but found '${Region}' in amplify-meta.json.`);
        }
        const { envName } = context.amplify.getEnvInfo();
        const baseUrl: string = providerPlugin.adminBackendMap[region].amplifyAdminUrl;
        url = `${baseUrl}/admin/${appId}/${envName}/datastore`;
      }
    } else {
      // Elastic Container API
      const {
        output: { PipelineName, ServiceName, ClusterName },
      } = selectedResource;
      const codePipeline = 'CodePipeline';
      const elasticContainer = 'ElasticContainer';

      const { selectedConsole } = await inquirer.prompt({
        name: 'selectedConsole',
        message: 'Which console you want to open',
        type: 'list',
        choices: [
          {
            name: 'Elastic Container Service (Deployed container status)',
            value: elasticContainer,
          },
          {
            name: 'CodePipeline (Container build status)',
            value: codePipeline,
          },
        ],
      });

      if (selectedConsole === elasticContainer) {
        url = `https://console.aws.amazon.com/ecs/home?region=${Region}#/clusters/${ClusterName}/services/${ServiceName}/details`;
      } else if (selectedConsole === codePipeline) {
        url = `https://${Region}.console.aws.amazon.com/codesuite/codepipeline/pipelines/${PipelineName}/view`;
      } else {
        printer.error('Option not available');
        return;
      }
    }

    await open(url, { wait: false });
  } else {
    printer.error('AppSync API is not pushed in the cloud.');
  }
};

export const serviceApiInputWalkthrough = async (context: $TSContext, serviceMetadata) => {
  let continuePrompt = false;
  let authConfig;
  let defaultAuthType;
  let resolverConfig;
  const { amplify } = context;
  const { inputs } = serviceMetadata;
  const allDefaultValues = getAllDefaults(amplify.getProjectDetails());

  let resourceAnswers = {};
  resourceAnswers[inputs[1].key] = allDefaultValues[inputs[1].key];
  resourceAnswers[inputs[0].key] = resourceAnswers[inputs[1].key];

  //
  // Default authConfig - API Key (expires in 7 days)
  //
  authConfig = {
    defaultAuthentication: {
      apiKeyConfig: {
        apiKeyExpirationDays: 7,
      },
      authenticationType: 'API_KEY',
    },
    additionalAuthenticationProviders: [],
  };

  //
  // Repeat prompt until user selects Continue
  //
  while (!continuePrompt) {
    const getAuthModeChoice = async () => {
      if (authConfig.defaultAuthentication.authenticationType === 'API_KEY') {
        return `${
          authProviderChoices.find((choice) => choice.value === authConfig.defaultAuthentication.authenticationType).name
        } (default, expiration time: ${authConfig.defaultAuthentication.apiKeyConfig.apiKeyExpirationDays} days from now)`;
      }
      return `${authProviderChoices.find((choice) => choice.value === authConfig.defaultAuthentication.authenticationType).name} (default)`;
    };

    const getAdditionalAuthModeChoices = async () => {
      let additionalAuthModesText = '';
      authConfig.additionalAuthenticationProviders.map(async (authMode) => {
        additionalAuthModesText += `, ${authProviderChoices.find((choice) => choice.value === authMode.authenticationType).name}`;
      });
      return additionalAuthModesText;
    };

    const basicInfoQuestionChoices = [];

    basicInfoQuestionChoices.push({
      name: chalk`{bold Name:} ${resourceAnswers[inputs[1].key]}`,
      value: 'API_NAME',
    });

    basicInfoQuestionChoices.push({
      name: chalk`{bold Authorization modes:} ${await getAuthModeChoice()}${await getAdditionalAuthModeChoices()}`,
      value: 'API_AUTH_MODE',
    });

    basicInfoQuestionChoices.push({
      name: chalk`{bold Conflict detection (required for DataStore):} ${resolverConfig?.project ? 'Enabled' : 'Disabled'}`,
      value: 'CONFLICT_DETECTION',
    });

    if (resolverConfig?.project) {
      basicInfoQuestionChoices.push({
        name: chalk`{bold Conflict resolution strategy:} ${
          conflictResolutionHanlderChoices.find((x) => x.value === resolverConfig.project.ConflictHandler).name
        }`,
        value: 'CONFLICT_STRATEGY',
      });
    }

    basicInfoQuestionChoices.push({
      name: 'Continue',
      value: 'CONTINUE',
    });

    const basicInfoQuestion = {
      type: 'list',
      name: 'basicApiSettings',
      message: 'Here is the GraphQL API that we will create. Select a setting to edit or continue',
      default: 'CONTINUE',
      choices: basicInfoQuestionChoices,
    };

    let { basicApiSettings } = await inquirer.prompt([basicInfoQuestion]);

    switch (basicApiSettings) {
      case 'API_NAME': {
        const resourceQuestions = [
          {
            type: inputs[1].type,
            name: inputs[1].key,
            message: inputs[1].question,
            validate: amplify.inputValidation(inputs[1]),
            default: () => {
              const defaultValue = allDefaultValues[inputs[1].key];
              return defaultValue;
            },
          },
        ];
        // API name question
        resourceAnswers = await inquirer.prompt(resourceQuestions);
        resourceAnswers[inputs[0].key] = resourceAnswers[inputs[1].key];
        allDefaultValues[inputs[1].key] = resourceAnswers[inputs[1].key];
        break;
      }
      case 'API_AUTH_MODE':
        // Ask additonal questions
        ({ authConfig, defaultAuthType } = await askDefaultAuthQuestion(context));
        ({ authConfig } = await askAdditionalQuestions(context, authConfig, defaultAuthType));
        break;
      case 'CONFLICT_DETECTION':
        resolverConfig = await askResolverConflictQuestion(context, resolverConfig);
        break;
      case 'CONFLICT_STRATEGY':
        resolverConfig = await askResolverConflictHandlerQuestion(context);
        break;
      case 'CONTINUE':
        continuePrompt = true;
        break;
    }
  }

  return {
    answers: resourceAnswers,
    output: {
      authConfig,
    },
    resolverConfig,
  };
};

const updateApiInputWalkthrough = async (context: $TSContext, project: Record<string, any>, resolverConfig, modelTypes) => {
  let authConfig;
  let defaultAuthType;
  const updateChoices = [
    {
      name: 'Authorization modes',
      value: 'AUTH_MODE',
    },
  ];
  // check if DataStore is enabled for the entire API
  if (project.config && !_.isEmpty(project.config.ResolverConfig)) {
    updateChoices.push({
      name: 'Conflict resolution strategy',
      value: 'CONFLICT_STRATEGY',
    });
    updateChoices.push({
      name: 'Disable conflict detection',
      value: 'DISABLE_CONFLICT',
    });
  } else {
    updateChoices.push({
      name: 'Enable conflict detection (required for DataStore)',
      value: 'ENABLE_CONFLICT',
    });
  }

  const updateOptionQuestion = {
    type: 'list',
    name: 'updateOption',
    message: 'Select a setting to edit',
    choices: updateChoices,
  };

  const { updateOption } = await inquirer.prompt([updateOptionQuestion]);

  if (updateOption === 'ENABLE_CONFLICT') {
    resolverConfig = await askResolverConflictHandlerQuestion(context, modelTypes);
  } else if (updateOption === 'DISABLE_CONFLICT') {
    resolverConfig = {};
  } else if (updateOption === 'AUTH_MODE') {
    ({ authConfig, defaultAuthType } = await askDefaultAuthQuestion(context));
    authConfig = await askAdditionalAuthQuestions(context, authConfig, defaultAuthType);
  } else if (updateOption === 'CONFLICT_STRATEGY') {
    resolverConfig = await askResolverConflictHandlerQuestion(context, modelTypes);
  }

  return {
    authConfig,
    resolverConfig,
  };
};

export const serviceWalkthrough = async (context: $TSContext, serviceMetadata: Record<string, any>) => {
  const resourceName = resourceAlreadyExists();
  const transformerVersion = await ApiCategoryFacade.getTransformerVersion(context);
  await addLambdaAuthorizerChoice(context);

  if (resourceName) {
    const errMessage =
      'You already have an AppSync API in your project. Use the "amplify update api" command to update your existing AppSync API.';
    printer.warn(errMessage);
    await context.usageData.emitError(new ResourceAlreadyExistsError(errMessage));
    exitOnNextTick(0);
  }

  const { amplify } = context;
  const { inputs } = serviceMetadata;

  const basicInfoAnswers = await serviceApiInputWalkthrough(context, serviceMetadata);
  let schemaContent = '';
  let askToEdit = true;

  // Schema template selection
  const schemaTemplateOptions = transformerVersion === 2 ? schemaTemplatesV2 : schemaTemplatesV1;
  const templateSelectionQuestion = {
    type: inputs[4].type,
    name: inputs[4].key,
    message: inputs[4].question,
    choices: schemaTemplateOptions.filter(templateSchemaFilter(basicInfoAnswers.output.authConfig)),
    validate: amplify.inputValidation(inputs[4]),
  };

  const { templateSelection } = await inquirer.prompt(templateSelectionQuestion);
  const schemaFilePath = path.join(graphqlSchemaDir, templateSelection);
  schemaContent += transformerVersion === 2 ? defineGlobalSandboxMode(getGraphQLTransformerAuthDocLink(transformerVersion)) : '';
  schemaContent += fs.readFileSync(schemaFilePath, 'utf8');

  return {
    ...basicInfoAnswers,
    noCfnFile: true,
    schemaContent,
    askToEdit,
  };
};

export const updateWalkthrough = async (context: $TSContext): Promise<UpdateApiRequest> => {
  const { allResources } = await context.amplify.getResourceStatus();
  let resourceDir;
  let resourceName;
  let resource;
  let authConfig;
  const resources = allResources.filter((resource) => resource.service === 'AppSync');
  await addLambdaAuthorizerChoice(context);

  // There can only be one appsync resource
  if (resources.length > 0) {
    resource = resources[0];
    if (resource.providerPlugin !== providerName) {
      // TODO: Move message string to seperate file
      throw new Error(
        `The selected resource is not managed using AWS Cloudformation. Please use the AWS AppSync Console to make updates to your API - ${resource.resourceName}`,
      );
    }
    ({ resourceName } = resource);
    resourceDir = pathManager.getResourceDirectoryPath(undefined, category, resourceName);
  } else {
    const errMessage = 'No AppSync resource to update. Use the "amplify add api" command to update your existing AppSync API.';
    printer.error(errMessage);
    await context.usageData.emitError(new ResourceDoesNotExistError(errMessage));
    exitOnNextTick(0);
  }

  // migrate API project
  await checkAppsyncApiResourceMigration(context, resourceName, true);

  // Get models
  const project = await readProjectConfiguration(resourceDir);
  let resolverConfig = project.config.ResolverConfig;

  await displayApiInformation(context, resource, project);

  // Check for common errors
  const directiveMap = collectDirectivesByTypeNames(project.schema);
  let modelTypes = [];

  if (directiveMap.types) {
    Object.keys(directiveMap.types).forEach((type) => {
      if (directiveMap.types[type].includes('model')) {
        modelTypes.push(type);
      }
    });
  }

  ({ authConfig, resolverConfig } = await updateApiInputWalkthrough(context, project, resolverConfig, modelTypes));

  return {
    version: 1,
    serviceModification: {
      serviceName: 'AppSync',
      defaultAuthType: authConfigToAppSyncAuthType(authConfig ? authConfig.defaultAuthentication : undefined),
      additionalAuthTypes:
        authConfig && authConfig.additionalAuthenticationProviders
          ? authConfig.additionalAuthenticationProviders.map(authConfigToAppSyncAuthType)
          : undefined,
      conflictResolution: resolverConfigToConflictResolution(resolverConfig),
    },
  };
};

async function displayApiInformation(context: $TSContext, resource: Record<string, any>, project: Record<string, any>) {
  let authModes: string[] = [];
  authModes.push(
    `- Default: ${await displayAuthMode(context, resource, resource.output.authConfig.defaultAuthentication.authenticationType)}`,
  );
  await resource.output.authConfig.additionalAuthenticationProviders?.map(async (authMode) => {
    authModes.push(`- ${await displayAuthMode(context, resource, authMode.authenticationType)}`);
  });

  printer.info('');

  printer.info('General information');
  printer.info('- Name: '.concat(resource.resourceName));
  if (resource?.output?.GraphQLAPIEndpointOutput) {
    printer.info(`- API endpoint: ${resource?.output?.GraphQLAPIEndpointOutput}`);
  }
  printer.info('');

  printer.info('Authorization modes');
  authModes.forEach((authMode) => printer.info(authMode));
  printer.info('');

  printer.info('Conflict detection (required for DataStore)');
  if (project.config && !_.isEmpty(project.config.ResolverConfig)) {
    printer.info(
      `- Conflict resolution strategy: ${
        conflictResolutionHanlderChoices.find((choice) => choice.value === project.config.ResolverConfig.project.ConflictHandler).name
      }`,
    );
  } else {
    printer.info('- Disabled');
  }

  printer.info('');
}

async function displayAuthMode(context: $TSContext, resource: Record<string, any>, authMode: string) {
  if (authMode === 'API_KEY' && resource.output.GraphQLAPIKeyOutput) {
    let { apiKeys } = await context.amplify.executeProviderUtils(context, 'awscloudformation', 'getGraphQLApiKeys', {
      apiId: resource.output.GraphQLAPIIdOutput,
    });
    let apiKeyExpires = apiKeys.find((key) => key.id == resource.output.GraphQLAPIKeyOutput)?.expires;
    if (!apiKeyExpires) {
      return authProviderChoices.find((choice) => choice.value === authMode).name;
    }
    let apiKeyExpiresDate = new Date(apiKeyExpires * 1000);
    return `${authProviderChoices.find((choice) => choice.value === authMode).name} expiring ${apiKeyExpiresDate}: ${
      resource.output.GraphQLAPIKeyOutput
    }`;
  }
  return authProviderChoices.find((choice) => choice.value === authMode).name;
}

async function askAdditionalQuestions(context: $TSContext, authConfig, defaultAuthType, modelTypes?) {
  authConfig = await askAdditionalAuthQuestions(context, authConfig, defaultAuthType);
  return { authConfig };
}

async function askResolverConflictQuestion(context: $TSContext, resolverConfig, modelTypes?) {
  let resolverConfigResponse: Record<string, any> = {};

  if (await context.prompt.confirm('Enable conflict detection?', !resolverConfig?.project)) {
    resolverConfigResponse = await askResolverConflictHandlerQuestion(context, modelTypes);
  }

  return resolverConfigResponse;
}

async function askResolverConflictHandlerQuestion(context: $TSContext, modelTypes?) {
  let resolverConfig: Record<string, any> = {};
  const askConflictResolutionStrategy = async (msg) => {
    let conflictResolutionStrategy;

    do {
      const conflictResolutionQuestion: ListQuestion = {
        type: 'list',
        name: 'conflictResolutionStrategy',
        message: msg,
        default: 'AUTOMERGE',
        choices: conflictResolutionHanlderChoices,
      };
      if (conflictResolutionStrategy === 'Learn More') {
        conflictResolutionQuestion.prefix = dataStoreLearnMore;
      }
      ({ conflictResolutionStrategy } = await inquirer.prompt([conflictResolutionQuestion]));
    } while (conflictResolutionStrategy === 'Learn More');

    let syncConfig: Record<string, any> = {
      ConflictHandler: conflictResolutionStrategy,
      ConflictDetection: 'VERSION',
    };

    if (conflictResolutionStrategy === 'LAMBDA') {
      const { newFunction, lambdaFunctionName } = await askSyncFunctionQuestion();
      syncConfig.LambdaConflictHandler = {
        name: lambdaFunctionName,
        new: newFunction,
      };
    }

    return syncConfig;
  };

  resolverConfig.project = await askConflictResolutionStrategy('Select the default resolution strategy');

  // Ask for per-model resolver override setting

  if (modelTypes && modelTypes.length > 0) {
    if (await context.prompt.confirm('Do you want to override default per model settings?', false)) {
      const modelTypeQuestion = {
        type: 'checkbox',
        name: 'selectedModelTypes',
        message: 'Select the models from below:',
        choices: modelTypes,
      };

      const { selectedModelTypes } = await inquirer.prompt([modelTypeQuestion]);

      if (selectedModelTypes.length > 0) {
        resolverConfig.models = {};
        for (const modelType of selectedModelTypes) {
          resolverConfig.models[modelType] = await askConflictResolutionStrategy(`Select the resolution strategy for ${modelType} model`);
        }
      }
    }
  }

  return resolverConfig;
}

async function askSyncFunctionQuestion() {
  const syncLambdaQuestion = {
    type: 'list',
    name: 'syncLambdaAnswer',
    message: 'Select from the options below',
    choices: [
      {
        name: 'Create a new Lambda Function',
        value: 'NEW',
      },
      {
        name: 'Existing Lambda Function',
        value: 'EXISTING',
      },
    ],
  };

  const { syncLambdaAnswer } = await inquirer.prompt([syncLambdaQuestion]);

  let lambdaFunctionName;
  const newFunction = syncLambdaAnswer === 'NEW';

  if (!newFunction) {
    const syncLambdaNameQuestion = {
      type: 'input',
      name: 'lambdaFunctionName',
      message: 'Enter lambda function name',
      validate: (val) => !!val,
    };
    ({ lambdaFunctionName } = await inquirer.prompt([syncLambdaNameQuestion]));
  }

  return { newFunction, lambdaFunctionName };
}

async function addLambdaAuthorizerChoice(context: $TSContext) {
  const transformerVersion = await ApiCategoryFacade.getTransformerVersion(context);
  if (transformerVersion === 2 && !authProviderChoices.some((choice) => choice.value == 'AWS_LAMBDA')) {
    authProviderChoices.push({
      name: 'Lambda',
      value: 'AWS_LAMBDA',
    });
  }
}

async function askDefaultAuthQuestion(context: $TSContext) {
  await addLambdaAuthorizerChoice(context);
  const currentAuthConfig = getAppSyncAuthConfig(stateManager.getMeta());
  const currentDefaultAuth =
    currentAuthConfig && currentAuthConfig.defaultAuthentication ? currentAuthConfig.defaultAuthentication.authenticationType : undefined;

  const defaultAuthTypeQuestion = {
    type: 'list',
    name: 'defaultAuthType',
    message: 'Choose the default authorization type for the API',
    choices: authProviderChoices,
    default: currentDefaultAuth,
  };

  const { defaultAuthType } = await inquirer.prompt([defaultAuthTypeQuestion]);

  // Get default auth configured
  const defaultAuth = await askAuthQuestions(defaultAuthType, context, false, currentAuthConfig?.defaultAuthentication);

  return {
    authConfig: {
      defaultAuthentication: defaultAuth,
    },
    defaultAuthType,
  };
}

export async function askAdditionalAuthQuestions(context: $TSContext, authConfig: Record<string, any>, defaultAuthType) {
  const currentAuthConfig = getAppSyncAuthConfig(stateManager.getMeta());
  authConfig.additionalAuthenticationProviders = [];
  if (await context.prompt.confirm('Configure additional auth types?')) {
    // Get additional auth configured
    const remainingAuthProviderChoices = authProviderChoices.filter((p) => p.value !== defaultAuthType);
    const currentAdditionalAuth = (
      (currentAuthConfig && currentAuthConfig.additionalAuthenticationProviders
        ? currentAuthConfig.additionalAuthenticationProviders
        : []) as any[]
    ).map((authProvider) => authProvider.authenticationType);

    const additionalProvidersQuestion: CheckboxQuestion = {
      type: 'checkbox',
      name: 'authType',
      message: 'Choose the additional authorization types you want to configure for the API',
      choices: remainingAuthProviderChoices,
      default: currentAdditionalAuth,
    };

    const additionalProvidersAnswer = await inquirer.prompt([additionalProvidersQuestion]);

    for (const authProvider of additionalProvidersAnswer.authType) {
      const config = await askAuthQuestions(
        authProvider,
        context,
        true,
        currentAuthConfig?.additionalAuthenticationProviders?.find((authSetting) => authSetting.authenticationType == authProvider),
      );

      authConfig.additionalAuthenticationProviders.push(config);
    }
  } else {
    authConfig.additionalAuthenticationProviders = (currentAuthConfig?.additionalAuthenticationProviders || []).filter(
      (p) => p.authenticationType !== defaultAuthType,
    );
  }
  return authConfig;
}

export async function askAuthQuestions(authType: string, context: $TSContext, printLeadText = false, authSettings) {
  if (authType === 'AMAZON_COGNITO_USER_POOLS') {
    if (printLeadText) {
      printer.info('Cognito UserPool configuration');
    }

    const userPoolConfig = await askUserPoolQuestions(context);

    return userPoolConfig;
  }

  if (authType === 'API_KEY') {
    if (printLeadText) {
      printer.info('API key configuration');
    }

    const apiKeyConfig = await askApiKeyQuestions(authSettings);

    return apiKeyConfig;
  }

  if (authType === 'AWS_IAM') {
    return {
      authenticationType: 'AWS_IAM',
    };
  }

  if (authType === 'OPENID_CONNECT') {
    if (printLeadText) {
      printer.info('OpenID Connect configuration');
    }

    const openIDConnectConfig = await askOpenIDConnectQuestions(authSettings);

    return openIDConnectConfig;
  }

  if (authType === 'AWS_LAMBDA') {
    if (printLeadText) {
      context.print.info('Lambda Authorizer configuration');
    }

    const lambdaConfig = await askLambdaQuestion(context);

    return lambdaConfig;
  }

  const errMessage = `Unknown authType: ${authType}`;
  printer.error(errMessage);
  await context.usageData.emitError(new UnknownResourceTypeError(errMessage));
  exitOnNextTick(1);
}

async function askUserPoolQuestions(context: $TSContext) {
  let authResourceName = checkIfAuthExists();
  if (!authResourceName) {
    authResourceName = await context.amplify.invokePluginMethod(context, 'auth', undefined, 'add', [context, true]);
  } else {
    printer.info('Use a Cognito user pool configured as a part of this project.');
  }

  // Added resources are prefixed with auth
  authResourceName = `auth${authResourceName}`;

  return {
    authenticationType: 'AMAZON_COGNITO_USER_POOLS',
    userPoolConfig: {
      userPoolId: authResourceName,
    },
  };
}

export async function askApiKeyQuestions(authSettings: Record<string, any> = undefined) {
  let defaultValues = {
    apiKeyExpirationDays: 7,
    description: '',
  };
  Object.assign(defaultValues, authSettings?.apiKeyConfig);

  const apiKeyQuestions = [
    {
      type: 'input',
      name: 'description',
      message: 'Enter a description for the API key:',
      default: defaultValues.description,
    },
    {
      type: 'input',
      name: 'apiKeyExpirationDays',
      message: 'After how many days from now the API key should expire (1-365):',
      default: defaultValues.apiKeyExpirationDays,
      validate: validateDays,
      // adding filter to ensure parsing input as int -> https://github.com/SBoudrias/Inquirer.js/issues/866
      filter: (value) => {
        const val = parseInt(value, 10);
        if (isNaN(val) || val <= 0 || val > 365) {
          return value;
        }
        return val;
      },
    },
  ];

  const apiKeyConfig: Record<string, any> = {};
  for (const apiKeyQuestion of apiKeyQuestions) {
    apiKeyConfig[apiKeyQuestion.name] = await prompter.input(apiKeyQuestion.message, { initial: apiKeyQuestion.default as string });
  }
  const apiKeyExpirationDaysNum = Number(apiKeyConfig.apiKeyExpirationDays);
  apiKeyConfig.apiKeyExpirationDate = Expiration.after(Duration.days(apiKeyExpirationDaysNum)).date;
  apiKeyConfig.apiKeyExpirationDays = apiKeyExpirationDaysNum;

  return {
    authenticationType: 'API_KEY',
    apiKeyConfig,
  };
}

async function askOpenIDConnectQuestions(authSettings: Record<string, any>) {
  let defaultValues = {
    authTTL: undefined,
    clientId: undefined,
    iatTTL: undefined,
    issuerUrl: undefined,
    name: undefined,
  };
  Object.assign(defaultValues, authSettings?.openIDConnectConfig);

  const openIDConnectQuestions = [
    {
      type: 'input',
      name: 'name',
      message: 'Enter a name for the OpenID Connect provider:',
      default: defaultValues.name,
    },
    {
      type: 'input',
      name: 'issuerUrl',
      message: 'Enter the OpenID Connect provider domain (Issuer URL):',
      validate: validateIssuerUrl,
      default: defaultValues.issuerUrl,
    },
    {
      type: 'input',
      name: 'clientId',
      message: 'Enter the Client Id from your OpenID Client Connect application (optional):',
      default: defaultValues.clientId,
    },
    {
      type: 'input',
      name: 'iatTTL',
      message: 'Enter the number of milliseconds a token is valid after being issued to a user:',
      validate: validateTTL,
      default: defaultValues.iatTTL,
    },
    {
      type: 'input',
      name: 'authTTL',
      message: 'Enter the number of milliseconds a token is valid after being authenticated:',
      validate: validateTTL,
      default: defaultValues.authTTL,
    },
  ];

  const openIDConnectConfig = await inquirer.prompt(openIDConnectQuestions);

  return {
    authenticationType: 'OPENID_CONNECT',
    openIDConnectConfig,
  };
}

async function validateDays(input: string) {
  const isValid = /^\d{0,3}$/.test(input);
  const days = isValid ? parseInt(input, 10) : 0;
  if (!isValid || days < 1 || days > 365) {
    return 'Number of days must be between 1 and 365.';
  }

  return true;
}

function validateIssuerUrl(input: string) {
  const isValid =
    /^(((?!http:\/\/(?!localhost))([a-zA-Z0-9.]{1,}):\/\/([a-zA-Z0-9-._~:?#@!$&'()*+,;=/]{1,})\/)|(?!http)(?!https)([a-zA-Z0-9.]{1,}):\/\/)$/.test(
      input,
    );

  if (!isValid) {
    return 'The value must be a valid URI with a trailing forward slash. HTTPS must be used instead of HTTP unless you are using localhost.';
  }

  return true;
}

function validateTTL(input: string) {
  const isValid = /^\d+$/.test(input);

  if (!isValid) {
    return 'The value must be a number.';
  }

  return true;
}

function resourceAlreadyExists() {
  const meta = stateManager.getMeta();
  let resourceName;

  if (meta[category]) {
    const categoryResources = meta[category];
    for (const resource of Object.keys(categoryResources)) {
      if (categoryResources[resource].service === serviceName) {
        resourceName = resource;
        break;
      }
    }
  }

  return resourceName;
}

export const migrate = async (context: $TSContext) => {
  await context.amplify.executeProviderUtils(context, 'awscloudformation', 'compileSchema', {
    forceCompile: true,
    migrate: true,
  });
};

export const getIAMPolicies = (resourceName: string, operations: string[]) => {
  let policy: Record<string, any> = {};
  const resources = [];
  const actions = [];
  if (!FeatureFlags.getBoolean('appSync.generateGraphQLPermissions')) {
    operations.forEach((crudOption) => {
      switch (crudOption) {
        case 'create':
          actions.push('appsync:Create*', 'appsync:StartSchemaCreation', 'appsync:GraphQL');
          resources.push(buildPolicyResource(resourceName, '/*'));
          break;
        case 'update':
          actions.push('appsync:Update*');
          break;
        case 'read':
          actions.push('appsync:Get*', 'appsync:List*');
          break;
        case 'delete':
          actions.push('appsync:Delete*');
          break;
        default:
          printer.info(`${crudOption} not supported`);
      }
    });
    resources.push(buildPolicyResource(resourceName, null));
  } else {
    actions.push('appsync:GraphQL');
    operations.forEach((operation) => resources.push(buildPolicyResource(resourceName, `/types/${operation}/*`)));
  }

  policy = {
    Effect: 'Allow',
    Action: actions,
    Resource: resources,
  };

  const attributes = ['GraphQLAPIIdOutput', 'GraphQLAPIEndpointOutput'];
  if (authConfigHasApiKey(getAppSyncAuthConfig(stateManager.getMeta()))) {
    attributes.push('GraphQLAPIKeyOutput');
  }

  return { policy, attributes };
};

const buildPolicyResource = (resourceName: string, path: string | null) => {
  return {
    'Fn::Join': [
      '',
      [
        'arn:aws:appsync:',
        { Ref: 'AWS::Region' },
        ':',
        { Ref: 'AWS::AccountId' },
        ':apis/',
        {
          Ref: `${category}${resourceName}GraphQLAPIIdOutput`,
        },
        ...(path ? [path] : []),
      ],
    ],
  };
};

const templateSchemaFilter = (authConfig) => {
  const authIncludesCognito = getAuthTypes(authConfig).includes('AMAZON_COGNITO_USER_POOLS');
  return (templateOption: ListChoiceOptions): boolean =>
    authIncludesCognito ||
    templateOption.name !== 'Objects with fine-grained access control (e.g., a project management app with owner-based authorization)';
};

const getAuthTypes = (authConfig) => {
  const additionalAuthTypes = (authConfig.additionalAuthenticationProviders || [])
    .map((provider) => provider.authenticationType)
    .filter((t) => !!t);

  const uniqueAuthTypes = new Set([...additionalAuthTypes, authConfig.defaultAuthentication.authenticationType]);

  return [...uniqueAuthTypes.keys()];
};

async function askLambdaQuestion(context) {
  const existingFunctions = functionsExist(context);
  const choices = [
    {
      name: 'Create a new Lambda function',
      value: 'newFunction',
    },
  ];
  if (existingFunctions) {
    choices.push({
      name: 'Use a Lambda function already added in the current Amplify project',
      value: 'projectFunction',
    });
  }

  let defaultFunctionType = 'newFunction';
  const lambdaAnswer = await inquirer.prompt({
    name: 'functionType',
    type: 'list',
    message: 'Choose a Lambda authorization function',
    choices,
    default: defaultFunctionType,
  });

  const { lambdaFunction } = await askLambdaSource(context, lambdaAnswer.functionType);
  const { ttlSeconds } = await inquirer.prompt({
    type: 'input',
    name: 'ttlSeconds',
    message: 'How long should the authorization response be cached in seconds?',
    validate: validateTTL,
    default: '300',
  });

  const lambdaAuthorizerConfig = {
    lambdaFunction,
    ttlSeconds,
  };

  return {
    authenticationType: 'AWS_LAMBDA',
    lambdaAuthorizerConfig,
  };
}

function functionsExist(context: $TSContext): boolean {
  const functionResources = context.amplify.getProjectDetails().amplifyMeta.function;
  if (!functionResources) {
    return false;
  }

  const lambdaFunctions = [];
  Object.keys(functionResources).forEach((resourceName) => {
    if (functionResources[resourceName].service === FunctionServiceNameLambdaFunction) {
      lambdaFunctions.push(resourceName);
    }
  });

  return lambdaFunctions.length !== 0;
}

async function askLambdaSource(context: $TSContext, functionType: string) {
  switch (functionType) {
    case 'projectFunction':
      return await askLambdaFromProject(context);
    case 'newFunction':
      return await newLambdaFunction(context);
    default:
      throw new Error(`Type ${functionType} not supported`);
  }
}

async function newLambdaFunction(context: $TSContext) {
  const resourceName = await createLambdaAuthorizerFunction(context);
  return { lambdaFunction: resourceName };
}

async function askLambdaFromProject(context: $TSContext) {
  const functionResources = context.amplify.getProjectDetails().amplifyMeta.function;
  const lambdaFunctions = [];
  Object.keys(functionResources).forEach((resourceName) => {
    if (functionResources[resourceName].service === FunctionServiceNameLambdaFunction) {
      lambdaFunctions.push(resourceName);
    }
  });

  const answer = await inquirer.prompt({
    name: 'lambdaFunction',
    type: 'list',
    message: 'Choose one of the Lambda functions',
    choices: lambdaFunctions,
    default: lambdaFunctions[0],
  });

  await context.amplify.invokePluginMethod(context, 'function', undefined, 'addAppSyncInvokeMethodPermission', [answer.lambdaFunction]);

  return { lambdaFunction: answer.lambdaFunction };
}

async function createLambdaAuthorizerFunction(context: $TSContext) {
  const [shortId] = uuid().split('-');
  const functionName = `graphQlLambdaAuthorizer${shortId}`;
  const resourceName = await context.amplify.invokePluginMethod(context, 'function', undefined, 'add', [
    context,
    'awscloudformation',
    FunctionServiceNameLambdaFunction,
    {
      functionName,
      defaultRuntime: 'nodejs',
      providerContext: {
        provider: 'awscloudformation',
      },
      template: 'lambda-auth',
      skipAdvancedSection: true,
      skipNextSteps: true,
    },
  ]);

  context.print.success(`Successfully added ${resourceName} function locally`);
  await context.amplify.invokePluginMethod(context, 'function', undefined, 'addAppSyncInvokeMethodPermission', [resourceName]);
  return resourceName;
}
