'use strict';

const { mergeSchemas, addResolversToSchema } = require('@graphql-tools/schema');
const { pruneSchema } = require('@graphql-tools/utils');
const { makeSchema } = require('nexus');
const { prop, startsWith } = require('lodash/fp');

const { wrapResolvers } = require('./wrap-resolvers');
const {
  registerSingleType,
  registerCollectionType,
  registerComponent,
  registerScalars,
  registerInternals,
  registerPolymorphicContentType,
  contentType: {
    registerEnumsDefinition,
    registerInputsDefinition,
    registerFiltersDefinition,
    registerDynamicZonesDefinition,
  },
} = require('./register-functions');

module.exports = ({ strapi }) => {
  const { service: getGraphQLService } = strapi.plugin('graphql');
  const { config } = strapi.plugin('graphql');

  const { KINDS, GENERIC_MORPH_TYPENAME } = getGraphQLService('constants');
  const extensionService = getGraphQLService('extension');

  // Type Registry
  let registry;
  // Builders Instances
  let builders;

  const buildSchema = () => {
    const isShadowCRUDEnabled = !!config('shadowCRUD');

    // Create a new empty type registry
    registry = getGraphQLService('type-registry').new();

    // Reset the builders instances associated to the
    // content-api, and link the new type registry
    builders = getGraphQLService('builders').new('content-api', registry);

    registerScalars({ registry, strapi });
    registerInternals({ registry, strapi });

    if (isShadowCRUDEnabled) {
      shadowCRUD();
    }

    // Build a merged schema from both Nexus types & SDL type definitions
    const schema = buildMergedSchema({ registry });

    // Generate the extension configuration for the content API.
    // This extension instance needs to be generated after the Nexus schema's
    // generation, so that configurations created during types definitions
    // can be registered before being used in the wrap resolvers operation
    const extension = extensionService.generate({ typeRegistry: registry });

    // Add the extension's resolvers to the final schema
    const schemaWithResolvers = addResolversToSchema(schema, extension.resolvers);

    // Create a configuration object for the artifacts generation
    const outputs = {
      schema: config('artifacts.schema', false),
      typegen: config('artifacts.typegen', false),
    };

    const currentEnv = strapi.config.get('environment');

    const nexusSchema = makeSchema({
      // Build the schema from the merged GraphQL schema.
      // Since we're passing the schema to the mergeSchema property, it'll transform our SDL type definitions
      // into Nexus type definition, thus allowing them to be handled by  Nexus plugins & other processing
      mergeSchema: { schema: schemaWithResolvers },

      // Apply user-defined plugins
      plugins: extension.plugins,

      // Whether to generate artifacts (GraphQL schema, TS types definitions) or not.
      // By default, we generate artifacts only on development environment
      shouldGenerateArtifacts: config('generateArtifacts', currentEnv === 'development'),

      // Artifacts generation configuration
      outputs,
    });

    // Wrap resolvers if needed (auth, middlewares, policies...) as configured in the extension
    const wrappedNexusSchema = wrapResolvers({ schema: nexusSchema, strapi, extension });

    // Prune schema, remove unused types
    // eg: removes registered subscriptions if they're disabled in the config)
    const prunedNexusSchema = pruneSchema(wrappedNexusSchema);

    return prunedNexusSchema;
  };

  const buildMergedSchema = ({ registry }) => {
    // Here we extract types, plugins & typeDefs from a temporary generated
    // extension since there won't be any addition allowed after schemas generation
    const { types, typeDefs = [] } = extensionService.generate({ typeRegistry: registry });

    // Nexus schema built with user-defined & shadow CRUD auto generated Nexus types
    const nexusSchema = makeSchema({ types: [registry.definitions, types] });

    // Merge type definitions with the Nexus schema
    return mergeSchemas({
      typeDefs,
      // Give access to the shadowCRUD & nexus based types
      // Note: This is necessary so that types defined in SDL can reference types defined with Nexus
      schemas: [nexusSchema],
    });
  };

  const shadowCRUD = () => {
    const extensionService = getGraphQLService('extension');

    // Get every content type & component defined in Strapi
    const contentTypes = [
      ...Object.values(strapi.components),
      ...Object.values(strapi.contentTypes),
    ];

    // Disable Shadow CRUD for admin content types
    contentTypes
      .map(prop('uid'))
      .filter(startsWith('admin::'))
      .forEach(uid => extensionService.shadowCRUD(uid).disable());

    const contentTypesWithShadowCRUD = contentTypes.filter(ct =>
      extensionService.shadowCRUD(ct.uid).isEnabled()
    );

    // Generate and register definitions for every content type
    registerAPITypes(contentTypesWithShadowCRUD);

    // Generate and register polymorphic types' definitions
    registerMorphTypes(contentTypesWithShadowCRUD);
  };

  /**
   * Register needed GraphQL types for every content type
   * @param {object[]} contentTypes
   */
  const registerAPITypes = contentTypes => {
    for (const contentType of contentTypes) {
      const { kind, modelType } = contentType;

      const registerOptions = { registry, strapi, builders };

      // Generate various types associated to the content type
      // (enums, dynamic-zones, filters, inputs...)
      registerEnumsDefinition(contentType, registerOptions);
      registerDynamicZonesDefinition(contentType, registerOptions);
      registerFiltersDefinition(contentType, registerOptions);
      registerInputsDefinition(contentType, registerOptions);

      // Generate & register component's definition
      if (modelType === 'component') {
        registerComponent(contentType, registerOptions);
      }

      // Generate & register single type's definition
      else if (kind === 'singleType') {
        registerSingleType(contentType, registerOptions);
      }

      // Generate & register collection type's definition
      else if (kind === 'collectionType') {
        registerCollectionType(contentType, registerOptions);
      }
    }
  };

  const registerMorphTypes = contentTypes => {
    // Create & register a union type that includes every type or component registered
    const genericMorphType = builders.buildGenericMorphDefinition();
    registry.register(GENERIC_MORPH_TYPENAME, genericMorphType, { kind: KINDS.morph });

    for (const contentType of contentTypes) {
      registerPolymorphicContentType(contentType, { registry, strapi });
    }
  };

  return { buildSchema };
};
