import * as path from 'path';
import {
  AccessControlMatrix,
  AuthRule,
  ModelOperation,
  MODEL_OPERATIONS,
  DEFAULT_GROUPS_FIELD,
  DEFAULT_GROUP_CLAIM,
  DEFAULT_OWNER_FIELD,
  getAuthDirectiveRules,
} from '@aws-amplify/graphql-auth-transformer';
import { $TSContext, stateManager, pathManager, FeatureFlags } from '@aws-amplify/amplify-cli-core';
import { parse, ObjectTypeDefinitionNode, DirectiveNode, FieldDefinitionNode } from 'graphql';
import { printer } from '@aws-amplify/amplify-prompts';
import { DirectiveWrapper } from '@aws-amplify/graphql-transformer-core';
import { readProjectSchema } from 'graphql-transformer-core';
import { getTransformerVersion } from '../graphql-transformer';

export const showApiAuthAcm = async (context: $TSContext, modelName: string): Promise<void> => {
  const providerPlugin = await import(context.amplify.getProviderPlugins(context)?.awscloudformation);
  const transformerVersion = await getTransformerVersion(context);

  if (transformerVersion < 2) {
    printer.error('This command requires version two or greater of the GraphQL transformer.');
    return;
  }

  const apiNames = Object.entries(stateManager.getMeta()?.api || {})
    .filter(([, apiResource]) => (apiResource as any).service === 'AppSync')
    .map(([name]) => name);

  if (apiNames.length === 0) {
    printer.info('No GraphQL API configured in the project. To add a GraphQL API run `amplify add api`.');
    return;
  }

  if (apiNames.length > 1) {
    // this condition should never hit as we only allow a single GraphQL API per project.
    printer.error(
      'You have multiple GraphQL APIs in the project. Only one GraphQL API is allowed per project. Run `amplify remove api` to remove an API.',
    );
    return;
  }

  // Do a full schema compilation to make sure we are not printing an ACM for an invalid schema
  try {
    await providerPlugin.compileSchema(context, {
      forceCompile: true,
    });
  } catch (error) {
    printer.warn('ACM generation requires a valid schema, the provided schema is invalid.');

    if (error.name) {
      printer.error(`${error.name}: ${error.message?.trim()}`);
    } else {
      printer.error(`An error has occurred during schema compilation: ${error.message?.trim()}`);
    }

    return;
  }

  const apiName = apiNames[0];
  const apiResourceDir = path.join(pathManager.getBackendDirPath(), 'api', apiName);
  const { schema } = await readProjectSchema(apiResourceDir);

  printACM(schema, modelName);
};

export function printACM(sdl: string, nodeName: string) {
  const schema = parse(sdl);
  const type = schema.definitions.find(
    (node) =>
      node.kind === 'ObjectTypeDefinition' && node.name.value === nodeName && node?.directives?.find((dir) => dir.name.value === 'model'),
  ) as ObjectTypeDefinitionNode;
  if (!type) {
    throw new Error(`Model "${nodeName}" does not exist.`);
  } else {
    const fields: string[] = type.fields!.map((field: FieldDefinitionNode) => field.name.value);
    const acm = new AccessControlMatrix({ name: type.name.value, operations: MODEL_OPERATIONS, resources: fields });
    const parentAuthDirective = type.directives?.find((dir) => dir.name.value === 'auth');
    if (parentAuthDirective) {
      const authRules: AuthRule[] = getAuthDirectiveRules(new DirectiveWrapper(parentAuthDirective), {
        isField: false,
        deepMergeArguments: FeatureFlags.getBoolean('graphqltransformer.shouldDeepMergeDirectiveConfigDefaults'),
      });
      convertModelRulesToRoles(acm, authRules);
    }
    for (const fieldNode of type.fields || []) {
      const fieldAuthDir = fieldNode.directives?.find((dir) => dir.name.value === 'auth') as DirectiveNode;
      if (fieldAuthDir) {
        if (parentAuthDirective) {
          acm.resetAccessForResource(fieldNode.name.value);
        }
        const authRules: AuthRule[] = getAuthDirectiveRules(new DirectiveWrapper(fieldAuthDir));
        convertModelRulesToRoles(acm, authRules, fieldNode.name.value);
      }
    }
    const truthTable = acm.getAcmPerRole();

    if (truthTable.size === 0) {
      printer.warn(`No auth rules have been configured for the "${type.name.value}" model.`);
    }

    for (const [role, acm] of truthTable) {
      console.group(role);
      console.table(acm);
      console.groupEnd();
    }
  }
}

function convertModelRulesToRoles(acm: AccessControlMatrix, authRules: AuthRule[], field?: string) {
  for (const rule of authRules) {
    const operations: ModelOperation[] = rule.operations || MODEL_OPERATIONS;
    if (rule.groups && !rule.groupsField) {
      rule.groups.forEach((group) => {
        const roleName = `${rule.provider}:staticGroup:${group}`;
        acm.setRole({ role: roleName, resource: field, operations });
      });
    } else {
      let roleName: string;
      switch (rule.provider) {
        case 'apiKey':
          roleName = 'apiKey:public';
          break;
        case 'iam':
          roleName = `iam:${rule.allow}`;
          break;
        case 'oidc':
        case 'userPools':
          if (rule.allow === 'groups') {
            const groupsField = rule.groupsField || DEFAULT_GROUPS_FIELD;
            const groupsClaim = rule.groupClaim || DEFAULT_GROUP_CLAIM;
            roleName = `${rule.provider}:dynamicGroup:${groupsClaim}:${groupsField}`;
          } else if (rule.allow === 'owner') {
            const ownerField = rule.ownerField || DEFAULT_OWNER_FIELD;
            roleName = `${rule.provider}:owner:${ownerField}`;
          } else if (rule.allow === 'private') {
            roleName = `${rule.provider}:${rule.allow}`;
          } else {
            throw new Error(`Could not create a role from ${JSON.stringify(rule)}`);
          }
          break;
        default:
          throw new Error(`Could not create a role from ${JSON.stringify(rule)}`);
      }
      acm.setRole({ role: roleName, resource: field, operations });
    }
  }
}
