import { beelinePlugin } from "./lib/beeline-graphql";
import { gql, makeExecutableSchema } from "apollo-server";
import { getUserIdFromToken as getUserIdFromTokenViaAuth0 } from "./auth";
import connectToDb from "./db";
import buildLoaders from "./loaders";
import { plugin as cacheControlPluginFork } from "./lib/apollo-cache-control-fork";
import {
  getAuthToken,
  getUserIdFromToken as getUserIdFromTokenViaDb,
} from "./auth-by-db";

const rootTypeDefs = gql`
  enum CacheScope {
    PUBLIC
    PRIVATE
  }
  directive @cacheControl(
    maxAge: Int
    staleWhileRevalidate: Int
    scope: CacheScope
  ) on FIELD_DEFINITION | OBJECT

  type Mutation
  type Query
`;

function mergeTypeDefsAndResolvers(modules) {
  const allTypeDefs = [];
  const allResolvers = {};

  for (const { typeDefs, resolvers } of modules) {
    allTypeDefs.push(typeDefs);
    for (const typeName of Object.keys(resolvers)) {
      allResolvers[typeName] = {
        ...allResolvers[typeName],
        ...resolvers[typeName],
      };
    }
  }

  return { typeDefs: allTypeDefs, resolvers: allResolvers };
}

const schema = makeExecutableSchema(
  mergeTypeDefsAndResolvers([
    { typeDefs: rootTypeDefs, resolvers: {} },
    require("./types/AppearanceLayer"),
    require("./types/ClosetList"),
    require("./types/Item"),
    require("./types/MutationsForSupport"),
    require("./types/Outfit"),
    require("./types/Pet"),
    require("./types/PetAppearance"),
    require("./types/User"),
    require("./types/Zone"),
  ])
);

const plugins = [cacheControlPluginFork({ calculateHttpHeaders: true })];

if (process.env["NODE_ENV"] !== "test") {
  plugins.push(beelinePlugin);
}

const config = {
  schema,
  context: async ({ req, res }) => {
    const db = await connectToDb();

    let authMode = req.headers["dti-auth-mode"] || "auth0";
    let currentUserId;
    if (authMode === "auth0") {
      const auth = (req && req.headers && req.headers.authorization) || "";
      const authMatch = auth.match(/^Bearer (.+)$/);
      const token = authMatch && authMatch[1];
      currentUserId = await getUserIdFromTokenViaAuth0(token);
    } else if (authMode === "db") {
      currentUserId = await getCurrentUserIdViaDb(req);
    } else {
      console.warn(
        `Unexpected auth mode: ${JSON.stringify(authMode)}. Skipping auth.`
      );
      currentUserId = null;
    }

    return {
      db,
      currentUserId,
      login: async (params) => {
        const authToken = await getAuthToken(params, db);
        if (authToken == null) {
          return null;
        }
        const oneWeekFromNow = new Date();
        oneWeekFromNow.setDate(oneWeekFromNow.getDate() + 7);
        res.setHeader(
          "Set-Cookie",
          `DTIAuthToken=${encodeURIComponent(JSON.stringify(authToken))}; ` +
            `Max-Age=${60 * 60 * 24 * 7}; Secure; HttpOnly; SameSite=Strict`
        );
        return authToken;
      },
      logout: async () => {
        // NOTE: This function isn't actually async in practice, but we mark it
        //       as such for consistency with `login`!
        // Set a header to delete the cookie. (That is, empty and expired.)
        res.setHeader("Set-Cookie", `DTIAuthToken=; Max-Age=-1`);
      },
      ...buildLoaders(db),
    };
  },
  formatResponse: (res, context) => {
    // The Authorization header can affect the response, so we signal that here
    // for caching user data! That way, login/logout will refresh user data,
    // even if it was briefly cached.
    //
    // NOTE: Our frontend JS only sends the Authorization header for user data
    //       queries. For public data, the header will be absent, and different
    //       users will still be able to share the same public cache data.
    //
    // NOTE: At time of writing, I'm not sure we use this in app? I think all
    //       current user data queries request fields with `maxAge: 0`. But I'm
    //       adding it just to remove a potential surprise gotcha later!
    context.response.http.headers.set("Vary", "Authorization");

    return res;
  },

  plugins,

  // We use our own fork of the cacheControl plugin!
  cacheControl: false,

  // Enable Playground in production :)
  introspection: true,
  playground: {
    endpoint: "/api/graphql",
  },
};

async function getCurrentUserIdViaDb(req) {
  const authTokenCookieString = req.cookies.DTIAuthToken;
  if (!authTokenCookieString) {
    return null;
  }

  let authTokenFromCookie = null;
  try {
    authTokenFromCookie = JSON.parse(authTokenCookieString);
  } catch (error) {
    console.warn(`DTIAuthToken cookie was not valid JSON, ignoring.`);
  }

  return await getUserIdFromTokenViaDb(authTokenFromCookie);
}

if (require.main === module) {
  const { ApolloServer } = require("apollo-server");
  const server = new ApolloServer(config);
  server.listen().then(({ url }) => {
    console.info(`🚀  Server ready at ${url}`);
  });
}

module.exports = { config };