const express = require("express");
const compression = require("compression");
const { graphqlHTTP } = require("express-graphql");
const expressPlayground =
  require("graphql-playground-middleware-express").default;
const { ApolloServer } = require("apollo-server-express");
const { PubSub } = require("graphql-subscriptions");
const { GooglePubSub } = require("@axelspringer/graphql-google-pubsub");
const schema = require("./schema/schema");
const mongoose = require("mongoose");
const cors = require("cors");
const cookieParser = require("cookie-parser");
const Appointment = require("./models/appointment");
const Client = require("./models/client");
const Employee = require("./models/employee");
const Notification = require("./models/notification");
const createNotificationFunction = require("./schema/mutations/notifications/createNotificationFunction");
const jwt = require("jsonwebtoken");
const createTokens = require("./createTokens");
const createAdminTokens = require("./createAdminTokens");
const passport = require("passport");
const FacebookStrategy = require("passport-facebook").Strategy;
const parseUrl = require("parseurl");
const getMainImage = require("./getMainImage");
const cron = require("node-cron");
const MessagingResponse = require("twilio").twiml.MessagingResponse;
const moment = require("moment");
const { v4: uuidv4 } = require("uuid");
const http = require("http");
const { ApiError, Environment } = require("square");
const SquareClient = require("square").Client;

// Used to normalize phone numbers for use by Twilio
const phone = require("phone");

// Fix Puppeteer memory leak issue
process.setMaxListeners(Infinity);

// Hide usernames and passwords
require("dotenv").config();

const app = express();

app.use(cookieParser());

// Compress all responses
app.use(compression());

// Prevent request entity too large errors
app.use(express.json({ limit: "50mb" }));

// Cross-Origin Requests
app.use(
  cors({
    origin:
      process.env.NODE_ENV === "production"
        ? process.env.PRODUCTION_CLIENT_URL
        : "http://localhost:3000",
    credentials: true,
  })
);

const port = process.env.PORT || 4000;

// Allow 200 responses, but not 304 not modified
app.disable("etag");

app.post("/api/customers", (req, res) => {
  res.setHeader(
    "Authorization",
    `Bearer ${process.env.SQUARE_SANDBOX_ACCESS_TOKEN}`
  );

  const requestParams = req.body;

  const client = new SquareClient({
    environment: Environment.Sandbox,
    accessToken: process.env.SQUARE_SANDBOX_ACCESS_TOKEN,
  });

  const { customersApi } = client;

  const idempotencyKey = uuidv4();

  const createCustomer = async () => {
    const requestBody = {
      idempotencyKey: idempotencyKey,
      givenName: requestParams.given_name,
      familyName: requestParams.family_name,
      emailAddress: requestParams.email_address,
      phoneNumber: requestParams.phone_number,
    };

    try {
      let { result } = await customersApi.createCustomer(requestBody);
      console.log(
        "API called successfully. Customer created successfully Returned data: " +
          result
      );
      res.send(result);
    } catch (error) {
      if (error instanceof ApiError) {
        console.log("Errors: ", error.errors);
        res.send(error.errors);
      } else {
        console.log("Unexpected Error: ", res.send(error));
      }
    }
  };

  createCustomer();
});

app.get("/smsresponse", async (req, res) => {
  const twiml = new MessagingResponse();

  const allApps = await Appointment.find({});
  const clientApps = allApps.filter(
    (appointment) => phone(appointment.client.phoneNumber)[0] === req.query.From
  );

  const upcomingClientApps = clientApps.filter((appointment) => {
    const date = moment(
      appointment.date +
        " " +
        appointment.startTime +
        " " +
        appointment.morningOrEvening,
      "MMMM D, YYYY h:mm A"
    );

    const now = moment();

    // Show upcoming unconfirmed appointments
    return date > now && !appointment.confirmed;
  });

  if (
    req.query.Body === "Y" ||
    req.query.Body === "y" ||
    req.query.Body === "Yes" ||
    req.query.Body === "YES" ||
    req.query.Body === "yes"
  ) {
    upcomingClientApps.forEach(async (item) => {
      let filter = {
        _id: item._id,
      };

      const update = {
        confirmed: true,
      };

      if (!item.confirmed) {
        const appointment = await Appointment.findOneAndUpdate(filter, update, {
          new: true,
        });

        const newNotification = new Notification({
          _id: new mongoose.Types.ObjectId(),
          new: true,
          type: "confirmAppointment",
          date: item.date,
          time: item.startTime + " " + item.morningOrEvening,
          associatedClientFirstName: item.client.firstName,
          associatedClientLastName: item.client.lastName,
          originalAssociatedStaffFirstName: item.esthetician.split(" ")[0],
          originalAssociatedStaffLastName: item.esthetician.split(" ")[1],
          createdByFirstName: item.client.firstName,
          createdByLastName: item.client.lastName,
          createdAt: Date.now(),
        });

        const updateNotifications = (staff) =>
          createNotificationFunction(newNotification, staff);

        (
          await Employee.find({
            employeeRole: "Admin",
            firstName: {
              $ne: item.esthetician.split(" ")[0],
            },
            lastName: { $ne: item.esthetician.split(" ")[1] },
          })
        ).forEach((currentEmployee) => {
          if (currentEmployee) {
            const notificationsObj = updateNotifications(currentEmployee);
            currentEmployee.notifications = notificationsObj.notifications;

            currentEmployee.save();
          }
        });

        const updatedEmployee = await Employee.findOne(
          {
            firstName: item.esthetician.split(" ")[0],
            lastName: item.esthetician.split(" ")[1],
          },
          (err, currentEmployee) => {
            if (currentEmployee) {
              const notificationsObj = updateNotifications(currentEmployee);
              currentEmployee.notifications = notificationsObj.notifications;

              currentEmployee.save();
            }
          }
        );

        updatedEmployee.save();
        appointment.save();
      }
    });

    if (upcomingClientApps.length === 1) {
      twiml.message("Thank you, your appointment has been confirmed!");
    } else if (upcomingClientApps.length > 1) {
      twiml.message("Thank you, your appointments have been confirmed!");
    } else {
      return null;
    }
  } else {
    return null;
  }

  res.writeHead(200, { "Content-Type": "text/xml" });
  res.end(twiml.toString());
});

// Schedule Twilio text appointment reminders
cron.schedule("* * * * *", async () => {
  const allApps = await Appointment.find({});
  const allAppsArr = allApps.map((appointment) => {
    return {
      id: appointment._id,
      client: appointment.client,
      startTime: appointment.startTime + " " + appointment.morningOrEvening,
      appointmentDate: appointment.date,
      dayPrior: moment(
        appointment.date +
          " " +
          appointment.startTime +
          " " +
          appointment.morningOrEvening,
        "MMMM D, YYYY h:mm A"
      )
        .subtract(1, "days")
        .format("MMMM D, YYYY h:mm A"),
      hourPrior: moment(
        appointment.date +
          " " +
          appointment.startTime +
          " " +
          appointment.morningOrEvening,
        "MMMM D, YYYY h:mm A"
      )
        .subtract(1, "hours")
        .format("MMMM D, YYYY h:mm A"),
      confirmed: appointment.confirmed,
    };
  });

  const currentDate = moment().format("MMMM D, YYYY h:mm A");

  const dayPriorMatchArr = allAppsArr.filter((x) => x.dayPrior === currentDate);
  const hourPriorMatchArr = allAppsArr.filter(
    (x) => x.hourPrior === currentDate
  );

  const accountSid = process.env.TWILIO_ACCOUNT_SID;
  const authToken = process.env.TWILIO_AUTH_TOKEN;
  const client = require("twilio")(accountSid, authToken);

  if (dayPriorMatchArr.length > 0) {
    dayPriorMatchArr.forEach((appointment) => {
      // Format phone number for Twilio texting purposes
      const clientPhoneNumber = phone(appointment.client.phoneNumber);

      client.messages
        .create({
          body:
            "Hi, " +
            appointment.client.firstName[0].toUpperCase() +
            appointment.client.firstName.slice(1).toLowerCase() +
            "! This is a reminder for your Glow Labs appointment tomorrow, " +
            moment(appointment.appointmentDate, "MMMM D, YYYY").format(
              "dddd, MMMM Do, YYYY"
            ) +
            " at " +
            appointment.startTime +
            ". " +
            (!appointment.confirmed ? "Reply Y to confirm." : "See you then!"),
          from: process.env.GLOW_LABS_TEXT_NUMBER,
          to:
            process.env.NODE_ENV === "production"
              ? clientPhoneNumber[0]
              : process.env.TWILIO_TEST_TEXT_NUMBER,
        })
        .then((message) => console.log(message.sid))
        .catch((err) => console.log(err));
    });
  } else if (hourPriorMatchArr.length > 0) {
    hourPriorMatchArr.forEach((appointment) => {
      // Format phone number for Twilio texting purposes
      const clientPhoneNumber = phone(appointment.client.phoneNumber);

      client.messages
        .create({
          body:
            "Hi, " +
            appointment.client.firstName[0].toUpperCase() +
            appointment.client.firstName.slice(1).toLowerCase() +
            "! We look forward to seeing you at your Glow Labs appointment today at " +
            appointment.startTime +
            ". " +
            (!appointment.confirmed
              ? "Reply Y to confirm."
              : "Have a great day!"),
          from: process.env.GLOW_LABS_TEXT_NUMBER,
          to:
            process.env.NODE_ENV === "production"
              ? clientPhoneNumber[0]
              : process.env.TWILIO_TEST_TEXT_NUMBER,
        })
        .then((message) => console.log(message.sid))
        .catch((err) => console.log(err));
    });
  } else {
    return null;
  }
});

app.post("/api/customers/card", (req, res) => {
  res.setHeader(
    "Authorization",
    `Bearer ${process.env.SQUARE_SANDBOX_ACCESS_TOKEN}`
  );
  const requestParams = req.body;

  const client = new SquareClient({
    environment: Environment.Sandbox,
    accessToken: process.env.SQUARE_SANDBOX_ACCESS_TOKEN,
  });

  const { customersApi } = client;

  const idempotencyKey = uuidv4();

  const customerId = requestParams.customerId;

  const createCard = async () => {
    const requestBody = {
      idempotencyKey: idempotencyKey,
      cardNonce: requestParams.card_nonce,
      billingAddress: requestParams.billing_address,
      cardholderName: requestParams.cardholder_name,
      verificationToken: requestParams.verification_token,
    };

    try {
      let { result } = await customersApi.createCustomerCard(
        customerId,
        requestBody
      );
      console.log(
        "API called successfully. Customer card created successfully. Returned data: " +
          result
      );
      res.send(result);
    } catch (error) {
      if (error instanceof ApiError) {
        console.log("Errors: ", error.errors);
        res.send({ error: error.errors });
      } else {
        console.log("Unexpected Error: ", res.send(error));
      }
    }
  };

  createCard();
});

app.post("/api/customers/delete_card", (req, res) => {
  res.setHeader(
    "Authorization",
    `Bearer ${process.env.SQUARE_SANDBOX_ACCESS_TOKEN}`
  );
  const requestParams = req.body;

  const client = new SquareClient({
    environment: Environment.Sandbox,
    accessToken: process.env.SQUARE_SANDBOX_ACCESS_TOKEN,
  });

  const { customersApi } = client;

  const customerId = requestParams.customerId;
  const cardId = requestParams.cardId;

  const deleteCard = async () => {
    try {
      let { result } = await customersApi.deleteCustomerCard(
        customerId,
        cardId
      );
      console.log("API called successfully. Returned data: " + result);
      res.send(result);
    } catch (error) {
      if (error instanceof ApiError) {
        console.log("Errors: ", error.errors);
        res.send(error.errors);
      } else {
        console.log("Unexpected Error: ", res.send(error));
      }
    }
  };

  deleteCard();
});

app.post("/api/retrieve_customer", (req, res) => {
  res.setHeader(
    "Authorization",
    `Bearer ${process.env.SQUARE_SANDBOX_ACCESS_TOKEN}`
  );

  const requestParams = req.body;

  const client = new SquareClient({
    environment: Environment.Sandbox,
    accessToken: process.env.SQUARE_SANDBOX_ACCESS_TOKEN,
  });

  const { customersApi } = client;

  const customerId = requestParams.data.squareCustomerId;

  const getCustomer = async () => {
    try {
      let { result } = await customersApi.retrieveCustomer(customerId);
      console.log("API called successfully. Returned data: " + result);
      res.send(result);
    } catch (error) {
      if (error instanceof ApiError) {
        console.log("Errors: ", error.errors);
        res.send(error.errors);
      } else {
        console.log("Unexpected Error: ", res.send(error));
      }
    }
  };

  getCustomer();
});

app.post("/api/delete_customer", (req, res) => {
  res.setHeader(
    "Authorization",
    `Bearer ${process.env.SQUARE_SANDBOX_ACCESS_TOKEN}`
  );
  const requestParams = req.body;

  const client = new SquareClient({
    environment: Environment.Sandbox,
    accessToken: process.env.SQUARE_SANDBOX_ACCESS_TOKEN,
  });

  const { customersApi } = client;

  const customerId = requestParams.data.squareCustomerId;

  const removeCustomer = async () => {
    try {
      let { result } = await customersApi.deleteCustomer(customerId);
      console.log("API called successfully. Returned data: " + result);
      res.send(result);
    } catch (error) {
      if (error instanceof ApiError) {
        console.log("Errors: ", error.errors);
        res.send(error.errors);
      } else {
        console.log("Unexpected Error: ", res.send(error));
      }
    }
  };

  removeCustomer();
});

app.use(async (req, res, next) => {
  let requestURL = req.originalUrl;
  let parsedURL = parseUrl(req).pathname;

  let urlArr = requestURL.split("");
  urlArr.splice(0, 1);
  let shortenedURL = urlArr.join("");

  let pathName = req.path.slice(1);

  let closingIndex;

  if (pathName.includes("https://")) {
    let url = pathName.slice(9);
    closingIndex = url.indexOf("/") + 10;
  } else if (pathName.includes("http://")) {
    let url = pathName.slice(8);
    closingIndex = url.indexOf("/") + 9;
  }

  const baseURL = req.path.slice(1, closingIndex);

  if (
    req.path.split("http://").length > 1 ||
    req.path.split("http://").join("").split("https://").length > 1
  ) {
    if (res.statusCode === 200) {
      let mainImage = await getMainImage(parsedURL, shortenedURL, baseURL)
        .then((data) => {
          return data;
        })
        .catch((err) => console.log(err));

      res.status(200).send({
        url: shortenedURL,
        image: mainImage,
      });
    } else {
      app.get(req.url, async (req, res) => {
        if (res.statusCode === 301) {
          let mainImage = await getMainImage(
            parsedURL,
            shortenedURL,
            baseURL
          ).then((data) => {
            return data;
          });

          return res.status(301).send({ url: shortenedURL, image: mainImage });
        } else if (res.statusCode === 302) {
          let mainImage = await getMainImage(
            parsedURL,
            shortenedURL,
            baseURL
          ).then((data) => {
            return data;
          });

          return res.status(302).send({ url: shortenedURL, image: mainImage });
        } else if (res.statusCode === 304) {
          let mainImage = await getMainImage(
            parsedURL,
            shortenedURL,
            baseURL
          ).then((data) => {
            return data;
          });

          return res.status(304).send({ url: shortenedURL, image: mainImage });
        }
      });
    }
    return next();
  }

  return next();
});

const googlePubSubOptions = {
  projectId: process.env.GOOGLE_PUB_SUB_PROJECT_ID,
  credentials: {
    client_email: process.env.GOOGLE_PUB_SUB_CLIENT_EMAIL,
    private_key: (
      process.env.GOOGLE_PUB_SUB_PRIVATE_KEY_PART_ONE +
      process.env.GOOGLE_PUB_SUB_PRIVATE_KEY_PART_TWO
    ).replace(new RegExp("\\\\n", "g"), "\n"),
  },
};

const pubsub =
  process.env.NODE_ENV === "production"
    ? new GooglePubSub(googlePubSubOptions)
    : new PubSub();

const server = new ApolloServer({
  schema,
  context: async ({ req, res }) => {
    return {
      req,
      res,
      pubsub,
    };
  },
  playground: process.env.NODE_ENV === "production" ? false : true,
});

passport.use(
  new FacebookStrategy(
    {
      clientID: `${process.env.FACEBOOK_APP_ID}`,
      clientSecret: `${process.env.FACEBOOK_APP_SECRET}`,
      callbackURL:
        process.env.NODE_ENV === "production"
          ? `${process.env.PRODUCTION_SERVER_URL}/api/auth/facebook/callback`
          : `http://localhost:${port}/api/auth/facebook/callback`,
      profileFields: [
        "emails",
        "first_name",
        "last_name",
        "picture.type(small)",
      ],
      passReqToCallback: true,
    },
    (req, accessToken, refreshToken, profile, done) => {
      if (accessToken) {
        req.isAuth = true;
        req.facebookAccessToken = accessToken;
        req.facebookProfile = profile._json;
      } else {
        req.isAuth = false;
      }
      return done();
    }
  )
);

app.get(
  "/api/auth/facebook",
  passport.authenticate("facebook", {
    authType: "rerequest",
    scope: ["email"],
  })
);

// Set guest consent form cookie upon accessing link from appointment email
app.get("/api/:id/consentform", async (req, res) => {
  const accessToken = req.cookies["access-token"];
  const refreshToken = req.cookies["refresh-token"];
  const dummyToken = req.cookies["dummy-token"];

  const client = await Client.findOne({ _id: req.params.id });

  if (client) {
    const generateGuestConsentFormAccessToken = (client) => {
      const token = jwt.sign(
        {
          id: req.params.id,
          auth: true,
        },
        process.env.JWT_SECRET_KEY_ACCESS,
        { expiresIn: "7d" }
      );
      return token;
    };

    const guestConsentFormAccessToken =
      generateGuestConsentFormAccessToken(client);

    if (!accessToken && !refreshToken && !dummyToken) {
      // Set Guest Consent Form Cookie
      res.cookie(
        "guest-consent-form-access-token",
        guestConsentFormAccessToken,
        {
          maxAge: 1000 * 60 * 60 * 24 * 7,
          secure: process.env.NODE_ENV === "production" ? true : false,
          domain:
            process.env.NODE_ENV === "production"
              ? process.env.PRODUCTION_CLIENT_ROOT
              : "localhost",
        }
      );
    }

    res.redirect(
      `${
        process.env.NODE_ENV === "production"
          ? process.env.PRODUCTION_CLIENT_URL
          : "http://localhost:3000"
      }/account/clientprofile/consentform/page1`
    );
  }
});

app.get("/api/auth/facebook/callback", (req, res, next) => {
  passport.authenticate("facebook", async (err, user, info) => {
    if (err) {
      return next(err);
    }

    let client;

    client = await Client.findOne({ email: req.facebookProfile.email });

    if (!client) {
      client = await Client.create({
        _id: new mongoose.mongo.ObjectID(),
        email: req.facebookProfile.email,
        firstName: req.facebookProfile.first_name,
        lastName: req.facebookProfile.last_name,
      });
    }

    const generateDummyToken = (client) => {
      const token = jwt.sign(
        {
          id: client._id,
          picture: req.facebookProfile.picture.data.url,
          auth: true,
        },
        process.env.JWT_SECRET_KEY_DUMMY,
        { expiresIn: "60d" }
      );
      return token;
    };

    const generateAccessToken = (client) => {
      const token = jwt.sign(
        {
          id: client._id,
          email: client.email,
          phoneNumber: client.phoneNumber,
          firstName: client.firstName,
          lastName: client.lastName,
          tokenCount: client.tokenCount,
        },
        process.env.JWT_SECRET_KEY_ACCESS,
        { expiresIn: "60d" }
      );
      return token;
    };

    const accessToken = generateAccessToken(client);
    const dummyToken = generateDummyToken(client);

    if (client) {
      req.isAuth = true;
      if (client.phoneNumber) {
        res.clearCookie("temporary-facebook-access-token", {
          domain:
            process.env.NODE_ENV === "production"
              ? process.env.PRODUCTION_CLIENT_ROOT
              : "localhost",
        });
        res.clearCookie("temporary-facebook-dummy-token", {
          domain:
            process.env.NODE_ENV === "production"
              ? process.env.PRODUCTION_CLIENT_ROOT
              : "localhost",
        });

        res.cookie("access-token", accessToken, {
          maxAge: 1000 * 60 * 60 * 24 * 60,
          httpOnly: true,
          secure: process.env.NODE_ENV === "production" ? true : false,
          domain:
            process.env.NODE_ENV === "production"
              ? process.env.PRODUCTION_CLIENT_ROOT
              : "localhost",
        });

        res.cookie("dummy-token", dummyToken, {
          maxAge: 1000 * 60 * 60 * 24 * 60,
          httpOnly: false,
          secure: process.env.NODE_ENV === "production" ? true : false,
          domain:
            process.env.NODE_ENV === "production"
              ? process.env.PRODUCTION_CLIENT_ROOT
              : "localhost",
        });
      } else {
        res.cookie("temporary-facebook-access-token", accessToken, {
          maxAge: 1000 * 60 * 15,
          httpOnly: true,
          secure: process.env.NODE_ENV === "production" ? true : false,
          domain:
            process.env.NODE_ENV === "production"
              ? process.env.PRODUCTION_CLIENT_ROOT
              : "localhost",
        });

        res.cookie("temporary-facebook-dummy-token", dummyToken, {
          maxAge: 1000 * 60 * 15,
          httpOnly: false,
          secure: process.env.NODE_ENV === "production" ? true : false,
          domain:
            process.env.NODE_ENV === "production"
              ? process.env.PRODUCTION_CLIENT_ROOT
              : "localhost",
        });
      }

      res.redirect(
        `${
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_URL
            : "http://localhost:3000"
        }/account/clientprofile`
      );
    } else {
      req.isAuth = false;
      res.redirect(
        `${
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_URL
            : "http://localhost:3000"
        }/account/login`
      );
    }
  })(req, res, next);
});

app.get("/", (req, res) => {
  res.send("The Glow Labs server is up and running!");
});

// Connect to MongoDB with Mongoose
mongoose
  .connect(
    `mongodb+srv://${process.env.MONGO_DB_USERNAME}:${process.env.MONGO_DB_PASSWORD}@glowlabs-qo7rk.mongodb.net/test?retryWrites=true&w=majority`,
    { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false }
  )
  .then(() => {
    console.log("Connected to MongoDB");
  })
  .catch((err) => console.log(err));

// Refresh logged-in client's tokens
app.use(async (req, res, next) => {
  const refreshToken = req.cookies["refresh-token"];
  const logoutCookie = req.cookies.logout;

  const generateDummyToken = (client) => {
    const token = jwt.sign(
      {
        id: client._id,
        auth: true,
      },
      process.env.JWT_SECRET_KEY_DUMMY,
      { expiresIn: "7d" }
    );
    return token;
  };

  if (refreshToken) {
    if (logoutCookie === undefined) {
      const refreshClient = jwt.verify(
        refreshToken,
        process.env.JWT_SECRET_KEY_REFRESH
      );

      const client = await Client.findOne({ email: refreshClient.email });

      const tokens = createTokens(client);
      res.clearCookie("access-token", {
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });
      res.clearCookie("refresh-token", {
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });
      res.clearCookie("dummy-token", {
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });

      const dummyToken = generateDummyToken(client);
      res.cookie("dummy-token", dummyToken, {
        maxAge: 1000 * 60 * 60 * 24 * 7,
        secure: process.env.NODE_ENV === "production" ? true : false,
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });

      res.cookie("access-token", tokens.accessToken, {
        maxAge: 1000 * 60 * 15,
        httpOnly: true,
        secure: process.env.NODE_ENV === "production" ? true : false,
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });

      res.cookie("refresh-token", tokens.refreshToken, {
        maxAge: 1000 * 60 * 60 * 24 * 7,
        httpOnly: true,
        secure: process.env.NODE_ENV === "production" ? true : false,
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });
    }
  }
  return next();
});

// Refresh logged-in employee's tokens
app.use(async (req, res, next) => {
  const adminRefreshToken = req.cookies["admin-refresh-token"];
  const logoutCookie = req.cookies.logout;

  const generateAdminDummyToken = (employee) => {
    const token = jwt.sign(
      {
        id: employee._id,
        employeeRole: employee.employeeRole,
        auth: true,
      },
      process.env.JWT_SECRET_KEY_DUMMY,
      { expiresIn: "7d" }
    );
    return token;
  };

  if (adminRefreshToken) {
    if (logoutCookie === undefined) {
      const refreshAdmin = jwt.verify(
        adminRefreshToken,
        process.env.JWT_SECRET_KEY_REFRESH
      );

      const employee = await Employee.findOne({
        email: refreshAdmin.email,
      });

      const tokens = createAdminTokens(employee);
      res.clearCookie("admin-access-token", {
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });
      res.clearCookie("admin-refresh-token", {
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });
      res.clearCookie("admin-dummy-token", {
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });

      const dummyToken = generateAdminDummyToken(employee);
      res.cookie("admin-dummy-token", dummyToken, {
        maxAge: 1000 * 60 * 60 * 24 * 7,
        secure: process.env.NODE_ENV === "production" ? true : false,
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });

      res.cookie("admin-access-token", tokens.accessToken, {
        maxAge: 1000 * 60 * 15,
        httpOnly: true,
        secure: process.env.NODE_ENV === "production" ? true : false,
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });

      res.cookie("admin-refresh-token", tokens.refreshToken, {
        maxAge: 1000 * 60 * 60 * 24 * 7,
        httpOnly: true,
        secure: process.env.NODE_ENV === "production" ? true : false,
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });
    }
  }
  return next();
});

// Set pubsub as universal context
app.use((req, res, next) => {
  req.pubsub = pubsub;
  return next();
});

// Handle client authentication
app.use(async (req, res, next) => {
  const accessToken = req.cookies["access-token"];
  const refreshToken = req.cookies["refresh-token"];
  const dummyToken = req.cookies["dummy-token"];
  const temporaryFacebookAccessToken =
    req.cookies["temporary-facebook-access-token"];
  const temporaryFacebookDummyToken =
    req.cookies["temporary-facebook-dummy-token"];
  const logoutCookie = req.cookies.logout;

  if (logoutCookie) {
    res.clearCookie("access-token", {
      domain:
        process.env.NODE_ENV === "production"
          ? process.env.PRODUCTION_CLIENT_ROOT
          : "localhost",
    });
    res.clearCookie("refresh-token", {
      domain:
        process.env.NODE_ENV === "production"
          ? process.env.PRODUCTION_CLIENT_ROOT
          : "localhost",
    });
    res.clearCookie("dummy-token", {
      domain:
        process.env.NODE_ENV === "production"
          ? process.env.PRODUCTION_CLIENT_ROOT
          : "localhost",
    });
    res.clearCookie("logout", {
      domain:
        process.env.NODE_ENV === "production"
          ? process.env.PRODUCTION_CLIENT_ROOT
          : "localhost",
    });
  }

  const generateDummyToken = (client) => {
    const token = jwt.sign(
      {
        id: client.id,
        auth: true,
      },
      process.env.JWT_SECRET_KEY_DUMMY,
      { expiresIn: "7d" }
    );
    return token;
  };

  if (!accessToken && !refreshToken && !temporaryFacebookAccessToken) {
    // No tokens in cookies
    req.isAuth = false;
    if (dummyToken) {
      res.clearCookie("dummy-token", {
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });
    }
    return next();
  } else {
    try {
      // Check validity/existence of access token
      // If valid access token, no need to check refresh token => USER AUTHENTICATED
      const accessClient = jwt.verify(
        accessToken,
        process.env.JWT_SECRET_KEY_ACCESS
      );
      req.isAuth = true;
      if (!dummyToken) {
        const dummyToken = generateDummyToken(accessClient);
        res.cookie("dummy-token", dummyToken, {
          maxAge: 1000 * 60 * 60 * 24 * 7,
          secure: process.env.NODE_ENV === "production" ? true : false,
          domain:
            process.env.NODE_ENV === "production"
              ? process.env.PRODUCTION_CLIENT_ROOT
              : "localhost",
        });
      }
      req.id = accessClient.id;
      return next();
    } catch {}

    // User does not have a valid access token / no access token => check refresh token
    if (!refreshToken) {
      // User does not have a refresh token and no temporary access token => UNAUTHENTICATED
      if (temporaryFacebookAccessToken) {
        req.isAuth = true;
        const client = await Client.findOne({
          _id: jwt.decode(temporaryFacebookAccessToken).id,
        });

        if (client.phoneNumber) {
          const generateFacebookDummyToken = (client) => {
            const token = jwt.sign(
              {
                id: client._id,
                picture: jwt.decode(temporaryFacebookDummyToken).picture,
                auth: true,
              },
              process.env.JWT_SECRET_KEY_DUMMY,
              { expiresIn: "60d" }
            );
            return token;
          };

          const generateFacebookAccessToken = (client) => {
            const token = jwt.sign(
              {
                id: client._id,
                email: client.email,
                phoneNumber: client.phoneNumber,
                firstName: client.firstName,
                lastName: client.lastName,
                tokenCount: client.tokenCount,
              },
              process.env.JWT_SECRET_KEY_ACCESS,
              { expiresIn: "60d" }
            );
            return token;
          };

          const accessToken = generateFacebookAccessToken(client);
          const dummyToken = generateFacebookDummyToken(client);

          res.clearCookie("temporary-facebook-access-token", {
            domain:
              process.env.NODE_ENV === "production"
                ? process.env.PRODUCTION_CLIENT_ROOT
                : "localhost",
          });
          res.clearCookie("temporary-facebook-dummy-token", {
            domain:
              process.env.NODE_ENV === "production"
                ? process.env.PRODUCTION_CLIENT_ROOT
                : "localhost",
          });

          res.cookie("access-token", accessToken, {
            maxAge: 1000 * 60 * 60 * 24 * 60,
            httpOnly: true,
            secure: process.env.NODE_ENV === "production" ? true : false,
            domain:
              process.env.NODE_ENV === "production"
                ? process.env.PRODUCTION_CLIENT_ROOT
                : "localhost",
          });

          res.cookie("dummy-token", dummyToken, {
            maxAge: 1000 * 60 * 60 * 24 * 60,
            httpOnly: false,
            secure: process.env.NODE_ENV === "production" ? true : false,
            domain:
              process.env.NODE_ENV === "production"
                ? process.env.PRODUCTION_CLIENT_ROOT
                : "localhost",
          });
        }
      } else {
        req.isAuth = false;
        if (dummyToken) {
          res.clearCookie("dummy-token", {
            domain:
              process.env.NODE_ENV === "production"
                ? process.env.PRODUCTION_CLIENT_ROOT
                : "localhost",
          });
        }
        if (temporaryFacebookDummyToken) {
          res.clearCookie("temporary-facebook-dummy-token", {
            domain:
              process.env.NODE_ENV === "production"
                ? process.env.PRODUCTION_CLIENT_ROOT
                : "localhost",
          });
        }
      }
      return next();
    }

    let refreshClient;

    // Check validity of refresh token
    try {
      refreshClient = jwt.verify(
        refreshToken,
        process.env.JWT_SECRET_KEY_REFRESH
      );
    } catch {
      // Refresh token is invalid
      req.isAuth = false;
      if (dummyToken) {
        res.clearCookie("dummy-token", {
          domain:
            process.env.NODE_ENV === "production"
              ? process.env.PRODUCTION_CLIENT_ROOT
              : "localhost",
        });
      }
      return next();
    }

    const client = await Client.findOne({ _id: refreshClient.id });

    // Refresh token is expired / not valid
    if (!client || client.tokenCount !== refreshClient.tokenCount) {
      req.isAuth = false;
      if (dummyToken) {
        res.clearCookie("dummy-token", {
          domain:
            process.env.NODE_ENV === "production"
              ? process.env.PRODUCTION_CLIENT_ROOT
              : "localhost",
        });
      }
      return next();
    }

    // Refresh token is valid => USER AUTHENTICATED and gets new refresh / access tokens
    req.isAuth = true;
    if (dummyToken) {
      res.clearCookie("dummy-token", {
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });
      const dummyToken = generateDummyToken(refreshClient);
      res.cookie("dummy-token", dummyToken, {
        maxAge: 1000 * 60 * 60 * 24 * 7,
        secure: process.env.NODE_ENV === "production" ? true : false,
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });
    }

    const tokens = createTokens(client);

    res.cookie("access-token", tokens.accessToken, {
      maxAge: 1000 * 60 * 15,
      httpOnly: true,
      secure: process.env.NODE_ENV === "production" ? true : false,
      domain:
        process.env.NODE_ENV === "production"
          ? process.env.PRODUCTION_CLIENT_ROOT
          : "localhost",
    });
    res.cookie("refresh-token", tokens.refreshToken, {
      maxAge: 1000 * 60 * 60 * 24 * 7,
      httpOnly: true,
      secure: process.env.NODE_ENV === "production" ? true : false,
      domain:
        process.env.NODE_ENV === "production"
          ? process.env.PRODUCTION_CLIENT_ROOT
          : "localhost",
    });
    req.id = client.id;
    return next();
  }
});

// Handle employee authentication
app.use(async (req, res, next) => {
  const accessToken = req.cookies["admin-access-token"];
  const refreshToken = req.cookies["admin-refresh-token"];
  const dummyToken = req.cookies["admin-dummy-token"];
  const temporaryAdminAccessToken = req.cookies["temporary-admin-access-token"];
  const temporaryAdminDummyToken = req.cookies["temporary-admin-dummy-token"];
  const logoutCookie = req.cookies.logout;

  if (logoutCookie) {
    res.clearCookie("admin-access-token", {
      domain:
        process.env.NODE_ENV === "production"
          ? process.env.PRODUCTION_CLIENT_ROOT
          : "localhost",
    });
    res.clearCookie("admin-refresh-token", {
      domain:
        process.env.NODE_ENV === "production"
          ? process.env.PRODUCTION_CLIENT_ROOT
          : "localhost",
    });
    res.clearCookie("admin-dummy-token", {
      domain:
        process.env.NODE_ENV === "production"
          ? process.env.PRODUCTION_CLIENT_ROOT
          : "localhost",
    });
    res.clearCookie("logout", {
      domain:
        process.env.NODE_ENV === "production"
          ? process.env.PRODUCTION_CLIENT_ROOT
          : "localhost",
    });
  }

  const generateAdminDummyToken = (employee) => {
    const token = jwt.sign(
      {
        id: employee._id,
        employeeRole: employee.employeeRole,
        auth: true,
      },
      process.env.JWT_SECRET_KEY_DUMMY,
      { expiresIn: "7d" }
    );
    return token;
  };

  if (!accessToken && !refreshToken && !temporaryAdminAccessToken) {
    // No employee tokens in cookies
    req.adminAuth = false;
    if (dummyToken) {
      res.clearCookie("admin-dummy-token", {
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });
    }
    return next();
  } else {
    try {
      // Check validity/existence of access token
      // If valid access token, no need to check refresh token => USER AUTHENTICATED
      const accessEmployee = jwt.verify(
        accessToken,
        process.env.JWT_SECRET_KEY_ACCESS
      );

      req.adminAuth = true;

      if (!dummyToken) {
        const dummyToken = generateAdminDummyToken(accessEmployee);
        res.cookie("admin-dummy-token", dummyToken, {
          maxAge: 1000 * 60 * 60 * 24 * 7,
          secure: process.env.NODE_ENV === "production" ? true : false,
          domain:
            process.env.NODE_ENV === "production"
              ? process.env.PRODUCTION_CLIENT_ROOT
              : "localhost",
        });
      }

      req.id = accessEmployee.id;
      return next();
    } catch {}

    // User does not have a valid access token / no access token => check refresh token
    if (!refreshToken) {
      // User does not have a refresh token and no temporary access token => UNAUTHENTICATED
      if (temporaryAdminAccessToken) {
        req.adminAuth = true;
        const employee = await Employee.findOne({
          _id: jwt.decode(temporaryAdminAccessToken).id,
        });

        if (employee.permanentPasswordSet) {
          const tokens = createAdminTokens(employee);
          res.clearCookie("temporary-admin-access-token", {
            domain:
              process.env.NODE_ENV === "production"
                ? process.env.PRODUCTION_CLIENT_ROOT
                : "localhost",
          });
          res.clearCookie("temporary-admin-dummy-token", {
            domain:
              process.env.NODE_ENV === "production"
                ? process.env.PRODUCTION_CLIENT_ROOT
                : "localhost",
          });

          const dummyToken = generateAdminDummyToken(employee);
          res.cookie("admin-dummy-token", dummyToken, {
            maxAge: 1000 * 60 * 60 * 24 * 7,
            secure: process.env.NODE_ENV === "production" ? true : false,
            domain:
              process.env.NODE_ENV === "production"
                ? process.env.PRODUCTION_CLIENT_ROOT
                : "localhost",
          });

          res.cookie("admin-access-token", tokens.accessToken, {
            maxAge: 1000 * 60 * 15,
            httpOnly: true,
            secure: process.env.NODE_ENV === "production" ? true : false,
            domain:
              process.env.NODE_ENV === "production"
                ? process.env.PRODUCTION_CLIENT_ROOT
                : "localhost",
          });

          res.cookie("admin-refresh-token", tokens.refreshToken, {
            maxAge: 1000 * 60 * 60 * 24 * 7,
            httpOnly: true,
            secure: process.env.NODE_ENV === "production" ? true : false,
            domain:
              process.env.NODE_ENV === "production"
                ? process.env.PRODUCTION_CLIENT_ROOT
                : "localhost",
          });
        }
      } else {
        req.adminAuth = false;
        if (dummyToken) {
          res.clearCookie("admin-dummy-token", {
            domain:
              process.env.NODE_ENV === "production"
                ? process.env.PRODUCTION_CLIENT_ROOT
                : "localhost",
          });
        }
        if (temporaryAdminDummyToken) {
          res.clearCookie("temporary-admin-dummy-token", {
            domain:
              process.env.NODE_ENV === "production"
                ? process.env.PRODUCTION_CLIENT_ROOT
                : "localhost",
          });
        }
      }
      return next();
    }

    let refreshAdmin;

    // Check validity of refresh token
    try {
      refreshAdmin = jwt.verify(
        refreshToken,
        process.env.JWT_SECRET_KEY_REFRESH
      );
    } catch {
      // Refresh token is invalid
      req.adminAuth = false;
      if (dummyToken) {
        res.clearCookie("admin-dummy-token", {
          domain:
            process.env.NODE_ENV === "production"
              ? process.env.PRODUCTION_CLIENT_ROOT
              : "localhost",
        });
      }
      return next();
    }

    const employee = await Employee.findOne({ _id: refreshAdmin.id });

    // Refresh token is expired / not valid
    if (!employee || employee.tokenCount !== refreshAdmin.tokenCount) {
      req.adminAuth = false;
      if (dummyToken) {
        res.clearCookie("admin-dummy-token", {
          domain:
            process.env.NODE_ENV === "production"
              ? process.env.PRODUCTION_CLIENT_ROOT
              : "localhost",
        });
      }
      return next();
    }

    // Refresh token is valid => USER AUTHENTICATED and gets new refresh / access tokens
    req.adminAuth = true;
    if (dummyToken) {
      res.clearCookie("admin-dummy-token", {
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });

      const dummyToken = generateAdminDummyToken(refreshAdmin);
      res.cookie("admin-dummy-token", dummyToken, {
        maxAge: 1000 * 60 * 60 * 24 * 7,
        secure: process.env.NODE_ENV === "production" ? true : false,
        domain:
          process.env.NODE_ENV === "production"
            ? process.env.PRODUCTION_CLIENT_ROOT
            : "localhost",
      });
    }

    const tokens = createAdminTokens(employee);

    res.cookie("admin-access-token", tokens.accessToken, {
      maxAge: 1000 * 60 * 15,
      httpOnly: true,
      secure: process.env.NODE_ENV === "production" ? true : false,
      domain:
        process.env.NODE_ENV === "production"
          ? process.env.PRODUCTION_CLIENT_ROOT
          : "localhost",
    });
    res.cookie("admin-refresh-token", tokens.refreshToken, {
      maxAge: 1000 * 60 * 60 * 24 * 7,
      httpOnly: true,
      secure: process.env.NODE_ENV === "production" ? true : false,
      domain:
        process.env.NODE_ENV === "production"
          ? process.env.PRODUCTION_CLIENT_ROOT
          : "localhost",
    });
    req.id = employee.id;
    return next();
  }
});

app.use(
  "/graphql",
  graphqlHTTP({
    schema,
    graphiql: process.env.NODE_ENV === "production" ? false : true,
  })
);

server.applyMiddleware({
  app,
});

app.get(
  "/playground",
  process.env.NODE_ENV === "production"
    ? (req, res, next) => res.send("No playground in production!")
    : expressPlayground({ endpoint: "/graphql" })
);

const httpServer = http.createServer(app);
server.installSubscriptionHandlers(httpServer);

httpServer.listen(port, () => {
  console.log(
    `🚀 Server ready at ${
      process.env.NODE_ENV === "production"
        ? process.env.PRODUCTION_SERVER_URL + server.graphqlPath
        : "http://localhost:" + port + server.graphqlPath
    }`
  );
  console.log(
    `🚀 Subscriptions ready at ${
      process.env.NODE_ENV === "production"
        ? "wss://" +
          process.env.PRODUCTION_SERVER_ROOT +
          server.subscriptionsPath
        : "ws://localhost:" + port + server.subscriptionsPath
    }`
  );
});