import express from 'express';
import { augmentTypeDefs, augmentSchema } from 'neo4j-graphql-js';
import bodyParser from 'body-parser';
import { ApolloServer, makeExecutableSchema } from 'apollo-server-express';
import { applyMiddleware } from 'graphql-middleware';
import * as neo4j from 'neo4j-driver';
import * as admin from 'firebase-admin';
import { tradeTokenForUser } from './auth';
import {
  typeDefs as defaultTypedefs,
  resolvers as defaultResolvers
} from './appConfigs/schema';

admin.initializeApp({
  credential: admin.credential.applicationDefault(),
  databaseURL: 'https://contact-tracking-app.firebaseio.com'
});

const typeDefs = process.env.TYPEDEFS || defaultTypedefs;
const resolvers = defaultResolvers;

export interface Context {
  user: User | null;
  driver: neo4j.Driver;
  req: Express.Request;
}

import errorMiddleware from './common/httpErrorHandler.middleware';
import helmet from 'helmet';
import { User } from './auth';
import permissions from './permissions';

const checkErrorHeaderMiddleware = async (req, res, next) => {
  req.error = req.headers['x-error'];
  next();
};

export default class App {
  public app: express.Application;
  public server: ApolloServer;
  public port: number;

  /**
   * @constructor
   * @param controllers
   * @param port
   */
  constructor(port: number) {
    const schema = makeExecutableSchema({
      typeDefs: augmentTypeDefs(typeDefs),
      resolverValidationOptions: {
        requireResolversForResolveType: false
      },
      resolvers
    });

    // Add auto-generated mutations
    const augmentedSchema = augmentSchema(schema, {
      query: {
        exclude: ['LogContactPayload', 'LogEntry']
      },
      mutation: {
        exclude: ['LogEntry']
      }
    });

    const schemaWithMiddleware = applyMiddleware(augmentedSchema, permissions);

    const driver = neo4j.driver(
      process.env.NEO4J_URI || 'bolt://localhost:7687',
      neo4j.auth.basic(
        process.env.NEO4J_USER || 'neo4j',
        process.env.NEO4J_PASSWORD || 'letmein'
      ),
      { encrypted: true }
    );

    this.app = express();
    this.server = new ApolloServer({
      schema: schemaWithMiddleware,
      engine: {
        apiKey: process.env.ENGINE_API_KEY || ''
      },
      // inject the request object into the context to support middleware
      // inject the Neo4j driver instance to handle database call
      context: async ({ req }): Promise<Context> => {
        const { authorization } = req.headers;
        const token =
          authorization && authorization.startsWith('Bearer ')
            ? authorization.slice(7, authorization.length)
            : '';

        const user = await tradeTokenForUser(token);
        return {
          user,
          driver,
          req
        };
      }
    });
    this.port = port;

    this.initializeMiddlewares();
    this.initializeErrorhandling();
  }

  /**
   * @func listen Make the server to listen at specified port.
   */
  public listen() {
    this.app.listen(this.port, () => {
      console.log(`server started at http://localhost:${this.port}/graphql`);
    });
  }

  /**
   * @func initializeMiddlewares Initializes all the middleware
   */
  private initializeMiddlewares() {
    this.app.use(bodyParser.json());
    this.app.use('*', checkErrorHeaderMiddleware);
    this.app.use(helmet());
    this.server.applyMiddleware({ app: this.app });
  }

  /**
   * @func initializeErrorhandling Initializes error handling middleware.
   */
  private initializeErrorhandling() {
    this.app.use(errorMiddleware);
  }
}