// This is the "batteries-included" version of `apollo-server-express`. It
// handles creating the Express app and HTTP server for you (using whatever
// version of `express` its dependency pulls in). If you need to customize the
// Express app or HTTP server at all, you just use `apollo-server-express`
// instead.
import express from 'express';
import http from 'http';
import {
  ApolloServer as ApolloServerExpress,
  CorsOptions,
  ApolloServerExpressConfig,
} from 'apollo-server-express';
import type { AddressInfo } from 'net';
import { format as urlFormat } from 'url';
import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core';

export * from './exports';

export interface ServerInfo {
  address: string;
  family: string;
  url: string;
  port: number | string;
  server: http.Server;
}

export class ApolloServer extends ApolloServerExpress {
  private cors: CorsOptions | boolean | undefined;
  private onHealthCheck: ((req: express.Request) => Promise<any>) | undefined;
  private healthCheckPath: string | null | undefined;
  private httpServer: http.Server;

  constructor(
    config: ApolloServerExpressConfig & {
      cors?: CorsOptions | boolean;
      onHealthCheck?: (req: express.Request) => Promise<any>;
      healthCheckPath?: string | null;
      stopGracePeriodMillis?: number;
    },
  ) {
    const httpServer = http.createServer();
    super({
      ...config,
      plugins: [
        ...(config.plugins ?? []),
        ApolloServerPluginDrainHttpServer({
          httpServer: httpServer,
          stopGracePeriodMillis: config.stopGracePeriodMillis,
        }),
      ],
    });

    this.httpServer = httpServer;
    this.cors = config.cors;
    this.onHealthCheck = config.onHealthCheck;
    this.healthCheckPath = config?.healthCheckPath;
  }

  private createServerInfo(): ServerInfo {
    const addressInfo = this.httpServer.address() as AddressInfo;

    // Convert IPs which mean "any address" (IPv4 or IPv6) into localhost
    // corresponding loopback ip. If this heuristic is wrong for your use case,
    // explicitly specify a frontend host (in the `host` option to
    // ApolloServer.listen).
    let hostForUrl = addressInfo.address;
    if (hostForUrl === '' || hostForUrl === '::') {
      hostForUrl = 'localhost';
    }

    const url = urlFormat({
      protocol: 'http',
      hostname: hostForUrl,
      port: addressInfo.port,
      pathname: this.graphqlPath,
    });

    return {
      ...addressInfo,
      server: this.httpServer,
      url,
    };
  }

  public override applyMiddleware() {
    throw new Error(
      'To use Apollo Server with an existing express application, please use apollo-server-express',
    );
  }

  public override async start(): Promise<void> {
    throw new Error(
      "When using the `apollo-server` package, you don't need to call start(); just call listen().",
    );
  }

  // Listen takes the same arguments as http.Server.listen.
  public async listen(...opts: Array<any>): Promise<ServerInfo> {
    // First start the server and throw if startup fails (eg, schema can't be loaded
    // or a serverWillStart plugin throws).
    await this._start();

    // This class is the easy mode for people who don't create their own express
    // object, so we have to create it.
    const app = express();
    this.httpServer.on('request', app);

    app.disable('x-powered-by');

    // provide generous values for the getting started experience
    super.applyMiddleware({
      app: app,
      path: '/',
      bodyParserConfig: { limit: '50mb' },
      onHealthCheck: this.onHealthCheck,
      cors:
        typeof this.cors !== 'undefined'
          ? this.cors
          : {
              origin: '*',
            },
      __internal_healthCheckPath: this.healthCheckPath,
    });

    await new Promise((resolve) => {
      this.httpServer.once('listening', resolve);
      // If the user passed a callback to listen, it'll get called in addition
      // to our resolver. They won't have the ability to get the ServerInfo
      // object unless they use our Promise, though.
      this.httpServer.listen(...(opts.length ? opts : [{ port: 4000 }]));
    });

    return this.createServerInfo();
  }
}
