import express from 'express';
import corsMiddleware from 'cors';
import { json, OptionsJson } from 'body-parser';
import {
  GraphQLOptions,
  ApolloServerBase,
  Config,
  runHttpQuery,
  convertNodeHttpToRequest,
  isHttpQueryError,
} from 'apollo-server-core';
import accepts from 'accepts';

export { GraphQLOptions } from 'apollo-server-core';

export interface GetMiddlewareOptions {
  path?: string;
  cors?:
    | corsMiddleware.CorsOptions
    | corsMiddleware.CorsOptionsDelegate
    | boolean;
  bodyParserConfig?: OptionsJson | boolean;
  onHealthCheck?: (req: express.Request) => Promise<any>;
  disableHealthCheck?: boolean;
  // There's no real point to allowing you to customize the health check path in
  // an Apollo Server web framework integration package. You're already using
  // Express --- just define a health check yourself by adding a handler that
  // returns 200 to the URL path of your choice. This option only exists to
  // provide a small amount of configuration for `apollo-server`, which doesn't
  // otherwise give you direct access to the web server. (Honestly, the health
  // check feature really should *only* exist in `apollo-server`; that it exists
  // elsewhere (and doesn't even check to see if GraphQL operations can
  // execute!) is a mistake we're stuck with due to backwards compatibility.)
  // Passing `null` here implies disableHealthCheck:true.
  __internal_healthCheckPath?: string | null;
}

export interface ServerRegistration extends GetMiddlewareOptions {
  // Note: You can also pass a connect.Server here. If we changed this field to
  // `express.Application | connect.Server`, it would be very hard to get the
  // app.use calls to typecheck even though they do work properly. Our
  // assumption is that very few people use connect with TypeScript (and in fact
  // we suspect the only connect users left writing GraphQL apps are Meteor
  // users).
  app: express.Application;
}

export interface ExpressContext {
  req: express.Request;
  res: express.Response;
}

export type ApolloServerExpressConfig = Config<ExpressContext>;

export class ApolloServer<
  ContextFunctionParams = ExpressContext,
> extends ApolloServerBase<ContextFunctionParams> {
  // This translates the arguments from the middleware into graphQL options It
  // provides typings for the integration specific behavior, ideally this would
  // be propagated with a generic to the super class
  async createGraphQLServerOptions(
    req: express.Request,
    res: express.Response,
  ): Promise<GraphQLOptions> {
    const contextParams: ExpressContext = { req, res };
    return super.graphQLServerOptions(contextParams);
  }

  public applyMiddleware({ app, ...rest }: ServerRegistration) {
    // getMiddleware calls this too, but we want the right method name in the error
    this.assertStarted('applyMiddleware');

    app.use(this.getMiddleware(rest));
  }

  // TODO: While `express` is not Promise-aware, this should become `async` in
  // a major release in order to align the API with other integrations (e.g.
  // Hapi) which must be `async`.
  public getMiddleware({
    path,
    cors,
    bodyParserConfig,
    disableHealthCheck,
    onHealthCheck,
    __internal_healthCheckPath,
  }: GetMiddlewareOptions = {}): express.Router {
    if (!path) path = '/graphql';
    this.assertStarted('getMiddleware');

    // Note that even though we use Express's router here, we still manage to be
    // Connect-compatible because express.Router just implements the same
    // middleware interface that Connect and Express share!
    const router = express.Router();

    if (!disableHealthCheck && __internal_healthCheckPath !== null) {
      router.use(
        __internal_healthCheckPath ?? '/.well-known/apollo/server-health',
        (req, res) => {
          // Response follows https://tools.ietf.org/html/draft-inadarei-api-health-check-01
          res.type('application/health+json');

          if (onHealthCheck) {
            onHealthCheck(req)
              .then(() => {
                res.json({ status: 'pass' });
              })
              .catch(() => {
                res.status(503).json({ status: 'fail' });
              });
          } else {
            res.json({ status: 'pass' });
          }
        },
      );
    }

    // XXX multiple paths?
    this.graphqlPath = path;

    // Note that we don't just pass all of these handlers to a single app.use call
    // for 'connect' compatibility.
    if (cors === true) {
      router.use(path, corsMiddleware<corsMiddleware.CorsRequest>());
    } else if (cors !== false) {
      router.use(path, corsMiddleware(cors));
    }

    if (bodyParserConfig === true) {
      router.use(path, json());
    } else if (bodyParserConfig !== false) {
      router.use(path, json(bodyParserConfig));
    }

    const landingPage = this.getLandingPage();
    router.use(path, (req, res, next) => {
      if (landingPage && prefersHtml(req)) {
        res.setHeader('Content-Type', 'text/html');
        res.write(landingPage.html);
        res.end();
        return;
      }

      if (!req.body) {
        // The json body-parser *always* sets req.body to {} if it's unset (even
        // if the Content-Type doesn't match), so if it isn't set, you probably
        // forgot to set up body-parser.
        res.status(500);
        if (bodyParserConfig === false) {
          res.send(
            '`res.body` is not set; you passed `bodyParserConfig: false`, ' +
              'but you still need to use `body-parser` middleware yourself.',
          );
        } else {
          res.send(
            '`res.body` is not set even though Apollo Server installed ' +
              "`body-parser` middleware; this shouldn't happen!",
          );
        }
        return;
      }

      runHttpQuery(
        [],
        {
          method: req.method,
          options: () => this.createGraphQLServerOptions(req, res),
          query: req.method === 'POST' ? req.body : req.query,
          request: convertNodeHttpToRequest(req),
        },
        this.csrfPreventionRequestHeaders,
      ).then(
        ({ graphqlResponse, responseInit }) => {
          if (responseInit.headers) {
            for (const [name, value] of Object.entries(responseInit.headers)) {
              res.setHeader(name, value);
            }
          }
          res.statusCode = responseInit.status || 200;

          // Using `.send` is a best practice for Express, but we also just use
          // `.end` for compatibility with `connect`.
          if (typeof res.send === 'function') {
            res.send(graphqlResponse);
          } else {
            res.end(graphqlResponse);
          }
        },
        (error: Error) => {
          if (!isHttpQueryError(error)) {
            return next(error);
          }

          if (error.headers) {
            for (const [name, value] of Object.entries(error.headers)) {
              res.setHeader(name, value);
            }
          }

          res.statusCode = error.statusCode;
          if (typeof res.send === 'function') {
            // Using `.send` is a best practice for Express, but we also just use
            // `.end` for compatibility with `connect`.
            res.send(error.message);
          } else {
            res.end(error.message);
          }
        },
      );
    });

    return router;
  }
}

function prefersHtml(req: express.Request): boolean {
  if (req.method !== 'GET') {
    return false;
  }
  const accept = accepts(req);
  const types = accept.types() as string[];
  return (
    types.find((x: string) => x === 'text/html' || x === 'application/json') ===
    'text/html'
  );
}
