import { Construct } from 'constructs';
import {
  AuthorizationType,
  CfnGraphQLApi,
  CfnGraphQLSchema,
  CfnApiKey,
  CfnResolver,
  CfnFunctionConfiguration,
  CfnDataSource,
  GraphqlApi,
  GraphqlApiAttributes,
  Visibility,
} from 'aws-cdk-lib/aws-appsync';
import { CfnTable, Table, ITable } from 'aws-cdk-lib/aws-dynamodb';
import { CfnRole, Role } from 'aws-cdk-lib/aws-iam';
import { CfnResource, isResolvableObject, NestedStack } from 'aws-cdk-lib';
import { getResourceName } from '@aws-amplify/graphql-transformer-core';
import { CfnFunction, Function as LambdaFunction } from 'aws-cdk-lib/aws-lambda';
import { AmplifyGraphqlApiResources, FunctionSlot } from '../types';
import { AmplifyDynamoDbTableWrapper } from '../amplify-dynamodb-table-wrapper';
import { walkAndProcessNodes } from './construct-tree';

/**
 * Check if a resource is implementing table interface
 * The required properties need to be present in the input
 * https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_dynamodb.ITable.html#properties
 * @param table table resource
 * @returns whether the resource is a ITable or not
 */
const isITable = (table: any): table is ITable => {
  return 'env' in table && 'node' in table && 'stack' in table && 'tableArn' in table && 'tableName' in table;
};

/**
 * Everything below here is intended to help us gather the
 * output values and render out the L1 resources for access.
 *
 * This is done by recursing along the construct tree, and classifying the generated resources.
 *
 * @param scope root to search for generated resource against
 * @returns a mapping of L1 and L2 constructs generated by the Graphql Transformer.
 */
export const getGeneratedResources = (scope: Construct): AmplifyGraphqlApiResources => {
  let cfnGraphqlApi: CfnGraphQLApi | undefined;
  let cfnGraphqlSchema: CfnGraphQLSchema | undefined;
  let cfnApiKey: CfnApiKey | undefined;
  const cfnResolvers: Record<string, CfnResolver> = {};
  const cfnFunctionConfigurations: Record<string, CfnFunctionConfiguration> = {};
  const cfnDataSources: Record<string, CfnDataSource> = {};
  const tables: Record<string, ITable> = {};
  const cfnTables: Record<string, CfnTable> = {};
  const amplifyDynamoDbTables: Record<string, AmplifyDynamoDbTableWrapper> = {};
  const roles: Record<string, Role> = {};
  const cfnRoles: Record<string, CfnRole> = {};
  const functions: Record<string, LambdaFunction> = {};
  const cfnFunctions: Record<string, CfnFunction> = {};
  const additionalCfnResources: Record<string, CfnResource> = {};

  const classifyConstruct = (currentScope: Construct): void => {
    if (currentScope instanceof CfnGraphQLApi) {
      cfnGraphqlApi = currentScope;
      return;
    }
    if (currentScope instanceof CfnGraphQLSchema) {
      cfnGraphqlSchema = currentScope;
      return;
    }
    if (currentScope instanceof CfnApiKey) {
      cfnApiKey = currentScope;
      return;
    }

    // Retrieve reference name for indexed resources, and bail if none is found.
    const resourceName = getResourceName(currentScope);
    if (!resourceName) return;

    if (currentScope instanceof CfnDataSource) {
      cfnDataSources[resourceName] = currentScope;
      return;
    }
    if (currentScope instanceof CfnResolver) {
      cfnResolvers[resourceName] = currentScope;
      return;
    }
    if (currentScope instanceof CfnFunctionConfiguration) {
      cfnFunctionConfigurations[resourceName] = currentScope;
      return;
    }
    if (currentScope instanceof Table || isITable(currentScope)) {
      tables[resourceName] = currentScope;
      return;
    }
    if (currentScope instanceof CfnTable) {
      cfnTables[resourceName] = currentScope;
      return;
    }
    if (AmplifyDynamoDbTableWrapper.isAmplifyDynamoDbTableResource(currentScope)) {
      amplifyDynamoDbTables[resourceName] = new AmplifyDynamoDbTableWrapper(currentScope);
      return;
    }
    if (currentScope instanceof Role) {
      roles[resourceName] = currentScope;
      return;
    }
    if (currentScope instanceof CfnRole) {
      cfnRoles[resourceName] = currentScope;
      return;
    }
    if (currentScope instanceof LambdaFunction) {
      functions[resourceName] = currentScope;
      return;
    }
    if (currentScope instanceof CfnFunction) {
      cfnFunctions[resourceName] = currentScope;
      return;
    }
    if (currentScope instanceof CfnResource) {
      additionalCfnResources[resourceName] = currentScope;
      return;
    }
  };

  scope.node.children.forEach((child) => walkAndProcessNodes(child, classifyConstruct));

  if (!cfnGraphqlApi) {
    throw new Error('Expected to find AWS::AppSync::GraphQLApi in the generated resource scope.');
  }

  if (!cfnGraphqlSchema) {
    throw new Error('Expected to find AWS::AppSync::GraphQLSchema in the generated resource scope.');
  }

  const nestedStacks: Record<string, NestedStack> = Object.fromEntries(
    scope.node.children.filter(NestedStack.isNestedStack).map((nestedStack: NestedStack) => [nestedStack.node.id, nestedStack]),
  );

  const proxiedApiAttributes = graphqlApiAttributesFromCfnGraphQLApi(cfnGraphqlApi);

  return {
    graphqlApi: GraphqlApi.fromGraphqlApiAttributes(scope, 'L2GraphqlApi', proxiedApiAttributes),
    tables,
    roles,
    functions,
    nestedStacks,
    cfnResources: {
      cfnGraphqlApi,
      cfnGraphqlSchema,
      cfnApiKey,
      cfnResolvers,
      cfnFunctionConfigurations,
      cfnDataSources,
      cfnTables,
      amplifyDynamoDbTables,
      cfnRoles,
      cfnFunctions,
      additionalCfnResources,
    },
  };
};

/**
 * Creates a set of L2 {@link GraphqlApiAttributes} from a CfnGraphqlApi L1 construct. Allows for getGeneratedResources to easily pass
 * attributes of the CfnGraphqlApi to the `resources` member. Without this the `resources.graphqlApi` member has no properties except for
 * the API ID.
 */
const graphqlApiAttributesFromCfnGraphQLApi = (cfnGraphqlApi: CfnGraphQLApi): GraphqlApiAttributes => {
  const visiblityStruct: { visibility?: Visibility } = {};
  if (typeof cfnGraphqlApi.visibility === 'string') {
    switch (cfnGraphqlApi.visibility) {
      case 'GLOBAL':
        visiblityStruct.visibility = Visibility.GLOBAL;
        break;
      case 'PRIVATE':
        visiblityStruct.visibility = Visibility.PRIVATE;
        break;
      default:
        console.warn(`Unsupported AppSync API Visibility setting: ${cfnGraphqlApi.visibility}`);
    }
  }

  const proxiedApiAttributes: GraphqlApiAttributes = {
    graphqlApiId: cfnGraphqlApi.attrApiId,
    graphqlApiArn: cfnGraphqlApi.attrArn,
    graphQLEndpointArn: cfnGraphqlApi.attrGraphQlEndpointArn,
    ...visiblityStruct,
    modes: authenticationTypesFromCfnGraphQLApi(cfnGraphqlApi),
  };

  return proxiedApiAttributes;
};

const authenticationTypesFromCfnGraphQLApi = (cfnGraphqlApi: CfnGraphQLApi): AuthorizationType[] => {
  const additionalAuthenticationProviders = cfnGraphqlApi.additionalAuthenticationProviders;
  if (!additionalAuthenticationProviders) {
    return [];
  }

  // If this is a deploy-time value rather than an array, we can't convert accurately.
  if (isResolvableObject(additionalAuthenticationProviders)) {
    return [];
  }

  const unfilteredAuthorizationTypes: (AuthorizationType | undefined)[] = additionalAuthenticationProviders
    .filter(
      (additionalAuthProvider): additionalAuthProvider is CfnGraphQLApi.AdditionalAuthenticationProviderProperty =>
        !isResolvableObject(additionalAuthProvider),
    )
    .map((provider) => provider.authenticationType)
    .map(l2AuthorizationTypeFromL1AuthenticationProvider);

  const authorizationTypes = unfilteredAuthorizationTypes.filter((type): type is AuthorizationType => typeof type !== 'undefined');

  const defaultAuthorizationType = l2AuthorizationTypeFromL1AuthenticationProvider(cfnGraphqlApi.authenticationType);
  if (defaultAuthorizationType) {
    authorizationTypes.push(defaultAuthorizationType);
  }
  return authorizationTypes;
};

const l2AuthorizationTypeFromL1AuthenticationProvider = (authenticationType: string): AuthorizationType | undefined => {
  switch (authenticationType) {
    case 'API_KEY':
      return AuthorizationType.API_KEY;
    case 'AWS_IAM':
      return AuthorizationType.IAM;
    case 'OPENID_CONNECT':
      return AuthorizationType.OIDC;
    case 'AMAZON_COGNITO_USER_POOLS':
      return AuthorizationType.USER_POOL;
    case 'AWS_LAMBDA':
      return AuthorizationType.LAMBDA;
    default:
      console.warn(`Unrecognized Authentication type ${authenticationType}`);
      return undefined;
  }
};

/**
 * Get the function slots generated by the Graphql transform operation, adhering to the FunctionSlot interface.
 * @param generatedResolvers the resolvers generated by the transformer to spit back out.
 * @returns the list of generated function slots in the transformer, in order to facilitate overrides.
 */
export const getGeneratedFunctionSlots = (generatedResolvers: Record<string, string>): FunctionSlot[] =>
  Object.entries(generatedResolvers)
    .filter(([name]) => name.split('.').length === 6)
    .map(([name, resolverCode]) => {
      const [typeName, fieldName, slotName, slotIndex, templateType] = name.split('.');
      return {
        typeName,
        fieldName,
        slotName,
        slotIndex: Number.parseInt(slotIndex, 10),
        function: {
          // TODO: this should consolidate req/req values back together
          ...(templateType === 'req' ? { requestMappingTemplate: resolverCode } : {}),
          ...(templateType === 'res' ? { responseMappingTemplate: resolverCode } : {}),
        },
      } as FunctionSlot;
    });
