// modified from vue-cli-plugin-apollo/graphql-server
// added a return value for the server() call

const http = require('http')
const { chalk } = require('@vue/cli-shared-utils')
const express = require('express')
const { ApolloServer, gql } = require('apollo-server-express')
const { PubSub } = require('graphql-subscriptions')
const merge = require('deepmerge')

function defaultValue (provided, value) {
  return provided == null ? value : provided
}

function autoCall (fn, ...context) {
  if (typeof fn === 'function') {
    return fn(...context)
  }
  return fn
}

module.exports = (options, cb = null) => {
  // Default options
  options = merge({
    integratedEngine: false
  }, options)

  // Express app
  const app = express()

  // Customize those files
  let typeDefs = load(options.paths.typeDefs)
  const resolvers = load(options.paths.resolvers)
  const context = load(options.paths.context)
  const schemaDirectives = load(options.paths.directives)
  let pubsub
  try {
    pubsub = load(options.paths.pubsub)
  } catch (e) {
    if (process.env.NODE_ENV !== 'production' && !options.quiet) {
      console.log(chalk.yellow('Using default PubSub implementation for subscriptions.'))
      console.log(chalk.grey('You should provide a different implementation in production (for example with Redis) by exporting it in \'apollo-server/pubsub.js\'.'))
    }
  }
  let dataSources
  try {
    dataSources = load(options.paths.dataSources)
  } catch (e) {}

  // GraphQL API Server

  // Realtime subscriptions
  if (!pubsub) pubsub = new PubSub()

  // Customize server
  try {
    const serverModule = load(options.paths.server)
    serverModule(app)
  } catch (e) {
    // No file found
  }

  // Apollo server options

  typeDefs = processSchema(typeDefs)

  let apolloServerOptions = {
    typeDefs,
    resolvers,
    schemaDirectives,
    dataSources,
    tracing: true,
    cacheControl: true,
    engine: !options.integratedEngine,
    // Resolvers context from POST
    context: async ({ req, connection }) => {
      let contextData
      try {
        if (connection) {
          contextData = await autoCall(context, { connection })
        } else {
          contextData = await autoCall(context, { req })
        }
      } catch (e) {
        console.error(e)
        throw e
      }
      contextData = Object.assign({}, contextData, { pubsub })
      return contextData
    },
    // Resolvers context from WebSocket
    subscriptions: {
      path: options.subscriptionsPath,
      onConnect: async (connection, websocket) => {
        let contextData = {}
        try {
          contextData = await autoCall(context, {
            connection,
            websocket
          })
          contextData = Object.assign({}, contextData, { pubsub })
        } catch (e) {
          console.error(e)
          throw e
        }
        return contextData
      }
    }
  }

  // Automatic mocking
  if (options.enableMocks) {
    // Customize this file
    apolloServerOptions.mocks = load(options.paths.mocks)
    apolloServerOptions.mockEntireSchema = false

    if (!options.quiet) {
      if (process.env.NODE_ENV === 'production') {
        console.warn('Automatic mocking is enabled, consider disabling it with the \'enableMocks\' option.')
      } else {
        console.log('✔️  Automatic mocking is enabled')
      }
    }
  }

  // Apollo Engine
  if (options.enableEngine && options.integratedEngine) {
    if (options.engineKey) {
      apolloServerOptions.engine = {
        apiKey: options.engineKey,
        schemaTag: options.schemaTag,
        ...options.engineOptions || {}
      }
      console.log('✔️  Apollo Engine is enabled')
    } else if (!options.quiet) {
      console.log(chalk.yellow('Apollo Engine key not found.') + `To enable Engine, set the ${chalk.cyan('VUE_APP_APOLLO_ENGINE_KEY')} env variable.`)
      console.log('Create a key at https://engine.apollographql.com/')
      console.log('You may see `Error: Must provide document` errors (query persisting tries).')
    }
  } else {
    apolloServerOptions.engine = false
  }

  // Final options
  apolloServerOptions = merge(apolloServerOptions, defaultValue(options.serverOptions, {}))

  // Apollo Server
  const server = new ApolloServer(apolloServerOptions)

  // Express middleware
  server.applyMiddleware({
    app,
    path: options.graphqlPath,
    cors: options.cors
    // gui: {
    //   endpoint: graphqlPath,
    //   subscriptionEndpoint: graphqlSubscriptionsPath,
    // },
  })

  // Start server
  const httpServer = http.createServer(app)
  httpServer.setTimeout(options.timeout)
  server.installSubscriptionHandlers(httpServer)

  httpServer.listen({
    host: options.host || 'localhost',
    port: options.port
  }, () => {
    if (!options.quiet) {
      console.log(`✔️  GraphQL Server is running on ${chalk.cyan(`http://localhost:${options.port}${options.graphqlPath}`)}`)
      if (process.env.NODE_ENV !== 'production' && !process.env.VUE_CLI_API_MODE) {
        console.log(`✔️  Type ${chalk.cyan('rs')} to restart the server`)
      }
    }

    cb && cb()
  })

  // added in order to let vue cli to deal with the http upgrade request
  return {
    apolloServer: server,
    httpServer
  }
}

function load (file) {
  const module = require(file)
  if (module.default) {
    return module.default
  }
  return module
}

function processSchema (typeDefs) {
  if (Array.isArray(typeDefs)) {
    return typeDefs.map(processSchema)
  }

  if (typeof typeDefs === 'string') {
    // Convert schema to AST
    typeDefs = gql(typeDefs)
  }

  // Remove upload scalar (it's already included in Apollo Server)
  removeFromSchema(typeDefs, 'ScalarTypeDefinition', 'Upload')

  return typeDefs
}

function removeFromSchema (document, kind, name) {
  const definitions = document.definitions
  const index = definitions.findIndex(
    def => def.kind === kind && def.name.kind === 'Name' && def.name.value === name
  )
  if (index !== -1) {
    definitions.splice(index, 1)
  }
}
