import * as path from 'path';
import fs from 'fs-extra';
import rimraf from 'rimraf';
import {
  $TSContext,
  AmplifyCategories,
  CloudformationProviderFacade,
  JSONUtilities,
  pathManager,
  stateManager,
} from '@aws-amplify/amplify-cli-core';
import { CloudFormation, Fn } from 'cloudform-types';
import { ResourceConstants } from 'graphql-transformer-common';
import { pullAllBy, find } from 'lodash';
import { printer } from '@aws-amplify/amplify-prompts';
import { DeploymentResources } from './cdk-compat/deployment-resources';
import { TransformerProjectConfig } from './cdk-compat/project-config';

const PARAMETERS_FILE_NAME = 'parameters.json';
const CUSTOM_ROLES_FILE_NAME = 'custom-roles.json';
const AMPLIFY_ADMIN_ROLE = '_Full-access/CognitoIdentityCredentials';
const AMPLIFY_MANAGE_ROLE = '_Manage-only/CognitoIdentityCredentials';
const PROVIDER_NAME = 'awscloudformation';

interface CustomRolesConfig {
  adminRoleNames?: Array<string> | string;
}

export const getIdentityPoolId = async (ctx: $TSContext): Promise<string | undefined> => {
  const { allResources, resourcesToBeDeleted } = await ctx.amplify.getResourceStatus('auth');
  const authResources = pullAllBy(allResources, resourcesToBeDeleted, 'resourceName');
  const authResource = find(authResources, { service: 'Cognito', providerPlugin: PROVIDER_NAME }) as any;
  return authResource?.output?.IdentityPoolId;
};

export const getAdminRoles = async (ctx: $TSContext, apiResourceName: string | undefined): Promise<Array<string>> => {
  let currentEnv;
  const adminRoles = new Array<string>();

  try {
    currentEnv = ctx.amplify.getEnvInfo().envName;
  } catch (err) {
    // When there is no environment info, return [] - This is required for sandbox pull
    return [];
  }

  // admin ui roles
  try {
    const amplifyMeta = stateManager.getMeta();
    const appId = amplifyMeta?.providers?.[PROVIDER_NAME]?.AmplifyAppId;
    const res = await CloudformationProviderFacade.isAmplifyAdminApp(ctx, appId);
    if (res.userPoolID) {
      adminRoles.push(`${res.userPoolID}${AMPLIFY_ADMIN_ROLE}`, `${res.userPoolID}${AMPLIFY_MANAGE_ROLE}`);
    }
  } catch (err) {
    // no need to error if not admin ui app
  }

  // additonal admin role checks
  if (apiResourceName) {
    // lambda functions which have access to the api
    const { allResources, resourcesToBeDeleted } = await ctx.amplify.getResourceStatus('function');
    const resources = pullAllBy(allResources, resourcesToBeDeleted, 'resourceName')
      .filter((r: any) => r.dependsOn?.some((d: any) => d?.resourceName === apiResourceName))
      .map((r: any) => `${r.resourceName}-${currentEnv}`);
    adminRoles.push(...resources);

    // check for custom iam admin roles
    const customRoleFile = path.join(
      pathManager.getResourceDirectoryPath(undefined, AmplifyCategories.API, apiResourceName),
      CUSTOM_ROLES_FILE_NAME,
    );
    if (fs.existsSync(customRoleFile)) {
      const customRoleConfig = JSONUtilities.readJson<CustomRolesConfig>(customRoleFile);
      if (customRoleConfig && customRoleConfig.adminRoleNames) {
        const customAdminRoles = Array.isArray(customRoleConfig.adminRoleNames)
          ? customRoleConfig.adminRoleNames
          : [customRoleConfig.adminRoleNames];
        const adminRoleNames = customAdminRoles
          // eslint-disable-next-line no-template-curly-in-string
          .map((r) => (r.includes('${env}') ? r.replace('${env}', currentEnv) : r));
        adminRoles.push(...adminRoleNames);
      }
    }
  }
  return adminRoles;
};

export function mergeUserConfigWithTransformOutput(
  userConfig: TransformerProjectConfig,
  transformOutput: DeploymentResources,
  opts?: any,
): DeploymentResources {
  const userFunctions = userConfig.functions || {};
  const userResolvers = userConfig.resolvers || {};
  const userPipelineFunctions = userConfig.pipelineFunctions || {};
  const { functions } = transformOutput;
  const { resolvers } = transformOutput;
  const { pipelineFunctions } = transformOutput;

  if (!opts?.disableFunctionOverrides) {
    for (const userFunction of Object.keys(userFunctions)) {
      functions[userFunction] = userFunctions[userFunction];
    }
  }

  if (!opts?.disablePipelineFunctionOverrides) {
    const pipelineFunctionKeys = Object.keys(userPipelineFunctions);

    if (pipelineFunctionKeys.length > 0) {
      printer.warn(
        ' You are using the "pipelineFunctions" directory for overridden and custom resolvers. ' +
          'Please use the "resolvers" directory as "pipelineFunctions" will be deprecated.\n',
      );
    }

    for (const userPipelineFunction of pipelineFunctionKeys) resolvers[userPipelineFunction] = userPipelineFunctions[userPipelineFunction];
  }

  if (!opts?.disableResolverOverrides) {
    for (const userResolver of Object.keys(userResolvers)) {
      if (userResolver !== 'README.md') {
        resolvers[userResolver] = userResolvers[userResolver].toString();
      }
    }
  }

  const stacks = overrideUserDefinedStacks(userConfig, transformOutput);

  return {
    ...transformOutput,
    functions,
    resolvers,
    pipelineFunctions,
    stacks,
  };
}

function overrideUserDefinedStacks(userConfig: TransformerProjectConfig, transformOutput: DeploymentResources) {
  const userStacks = userConfig.stacks || {};
  const { stacks, rootStack } = transformOutput;

  const resourceTypesToDependOn = {
    'AWS::CloudFormation::Stack': true,
    'AWS::AppSync::GraphQLApi': true,
    'AWS::AppSync::GraphQLSchema': true,
  };

  const allResourceIds = Object.keys(rootStack.Resources).filter((k: string) => {
    const resource = rootStack.Resources[k];
    return resourceTypesToDependOn[resource.Type];
  });

  const parametersKeys = Object.keys(rootStack.Parameters);
  const customStackParams = parametersKeys.reduce(
    (acc: any, k: string) => ({
      ...acc,
      [k]: Fn.Ref(k),
    }),
    {},
  );

  customStackParams[ResourceConstants.PARAMETERS.AppSyncApiId] = Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId');

  const updatedParameters = rootStack.Parameters;

  for (const userStack of Object.keys(userStacks)) {
    if (stacks[userStack]) {
      throw new Error(`You cannot provide a stack named ${userStack} as it \
            will be overwritten by a stack generated by the GraphQL Transform.`);
    }
    const userDefinedStack = userStacks[userStack];

    for (const key of Object.keys(userDefinedStack.Parameters)) {
      if (customStackParams[key] == null) {
        customStackParams[key] = Fn.Ref(key);

        if (updatedParameters[key]) throw new Error(`Cannot redefine CloudFormation parameter ${key} in stack ${userStack}.`);
        else updatedParameters[key] = userDefinedStack.Parameters[key];
      }
    }

    const parametersForStack = Object.keys(userDefinedStack.Parameters).reduce(
      (acc, k) => ({
        ...acc,
        [k]: customStackParams[k],
      }),
      {},
    );

    stacks[userStack] = userDefinedStack;

    const stackResourceId = userStack.split(/[^A-Za-z]/).join('');
    const customNestedStack = new CloudFormation.Stack({
      Parameters: parametersForStack,
      TemplateURL: Fn.Join('/', [
        'https://s3.amazonaws.com',
        Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
        Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
        'stacks',
        userStack,
      ]),
    }).dependsOn(allResourceIds);
    rootStack.Resources[stackResourceId] = customNestedStack;
  }

  rootStack.Parameters = updatedParameters;

  return stacks;
}

/**
 * Writes a deployment to disk at a path.
 */
export async function writeDeploymentToDisk(
  context: $TSContext,
  deployment: DeploymentResources,
  directory: string,
  rootStackFileName = 'rootStack.json',
  buildParameters: Object,
) {
  fs.ensureDirSync(directory);
  // Delete the last deployments resources except for tsconfig if present
  emptyBuildDirPreserveTsconfig(directory);

  // Write the schema to disk
  const { schema } = deployment;
  const fullSchemaPath = path.normalize(`${directory}/schema.graphql`);
  fs.writeFileSync(fullSchemaPath, schema);

  // Setup the directories if they do not exist.
  initStacksAndResolversDirectories(directory);

  // Write resolvers to disk
  const resolverFileNames = Object.keys(deployment.resolvers);
  const resolverRootPath = resolverDirectoryPath(directory);
  for (const resolverFileName of resolverFileNames) {
    const fullResolverPath = path.normalize(`${resolverRootPath}/${resolverFileName}`);
    fs.writeFileSync(fullResolverPath, deployment.resolvers[resolverFileName]);
  }

  // Write pipeline resolvers to disk
  const pipelineFunctions = Object.keys(deployment.pipelineFunctions);
  const pipelineFunctionRootPath = pipelineFunctionDirectoryPath(directory);
  for (const functionFileName of pipelineFunctions) {
    const fullTemplatePath = path.normalize(`${pipelineFunctionRootPath}/${functionFileName}`);
    fs.writeFileSync(fullTemplatePath, deployment.pipelineFunctions[functionFileName]);
  }

  // Write the stacks to disk
  const stackNames = Object.keys(deployment.stacks);
  const stackRootPath = stacksDirectoryPath(directory);
  for (const stackFileName of stackNames) {
    const fileNameParts = stackFileName.split('.');
    if (fileNameParts.length === 1) {
      fileNameParts.push('json');
    }
    const fullFileName = fileNameParts.join('.');
    throwIfNotJSONExt(fullFileName);
    const fullStackPath = path.normalize(`${stackRootPath}/${fullFileName}`);
    let stackContent = deployment.stacks[stackFileName];
    if (typeof stackContent === 'string') {
      stackContent = JSON.parse(stackContent);
    }
    await CloudformationProviderFacade.prePushCfnTemplateModifier(context, stackContent);
    fs.writeFileSync(fullStackPath, JSONUtilities.stringify(stackContent));
  }

  // Write any functions to disk
  const functionNames = Object.keys(deployment.functions);
  const functionRootPath = path.normalize(`${directory}/functions`);
  if (!fs.existsSync(functionRootPath)) {
    fs.mkdirSync(functionRootPath);
  }
  for (const functionName of functionNames) {
    const fullFunctionPath = path.normalize(`${functionRootPath}/${functionName}`);
    const zipContents = fs.readFileSync(deployment.functions[functionName]);
    fs.writeFileSync(fullFunctionPath, zipContents);
  }
  const { rootStack } = deployment;
  const rootStackPath = path.normalize(`${directory}/${rootStackFileName}`);
  const rootStackString = JSON.stringify(rootStack, null, 4);
  fs.writeFileSync(rootStackPath, rootStackString);

  // Write params to disk
  const jsonString = JSON.stringify(buildParameters, null, 4);
  const parametersOutputFilePath = path.join(directory, PARAMETERS_FILE_NAME);
  fs.writeFileSync(parametersOutputFilePath, jsonString);
}

function initStacksAndResolversDirectories(directory: string) {
  const resolverRootPath = resolverDirectoryPath(directory);
  if (!fs.existsSync(resolverRootPath)) {
    fs.mkdirSync(resolverRootPath);
  }
  const stackRootPath = stacksDirectoryPath(directory);
  if (!fs.existsSync(stackRootPath)) {
    fs.mkdirSync(stackRootPath);
  }
}

function pipelineFunctionDirectoryPath(rootPath: string) {
  return path.normalize(path.join(rootPath, 'pipelineFunctions'));
}

function resolverDirectoryPath(rootPath: string) {
  return path.normalize(`${rootPath}/resolvers`);
}

function stacksDirectoryPath(rootPath: string) {
  return path.normalize(`${rootPath}/stacks`);
}

function throwIfNotJSONExt(stackFile: string) {
  const extension = path.extname(stackFile);
  if (extension === '.yaml' || extension === '.yml') {
    throw new Error(`Yaml is not yet supported. Please convert the CloudFormation stack ${stackFile} to json.`);
  }
  if (extension !== '.json') {
    throw new Error(`Invalid extension ${extension} for stack ${stackFile}`);
  }
}

const emptyBuildDirPreserveTsconfig = (directory: string) => {
  const files = fs.readdirSync(directory);
  files.forEach((file) => {
    const fileDir = path.join(directory, file);
    if (fs.lstatSync(fileDir).isDirectory()) {
      rimraf.sync(fileDir);
    } else if (!file.endsWith('tsconfig.resource.json')) {
      fs.unlinkSync(fileDir);
    }
  });
};
