import passport from "@outlinewiki/koa-passport";
import type { Request } from "express";
import Router from "koa-router";
import { capitalize } from "lodash";
import { Profile } from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth2";
import { parseDomain } from "@shared/utils/domains";
import accountProvisioner, {
  AccountProvisionerResult,
} from "@server/commands/accountProvisioner";
import env from "@server/env";
import {
  GmailAccountCreationError,
  InviteRequiredError,
  TeamDomainRequiredError,
} from "@server/errors";
import passportMiddleware from "@server/middlewares/passport";
import { Team, User } from "@server/models";
import { StateStore, parseState } from "@server/utils/passport";

const router = new Router();
const providerName = "google";
const scopes = [
  "https://www.googleapis.com/auth/userinfo.profile",
  "https://www.googleapis.com/auth/userinfo.email",
];

export const config = {
  name: "Google",
  enabled: !!env.GOOGLE_CLIENT_ID,
};

type GoogleProfile = Profile & {
  email: string;
  picture: string;
  _json: {
    hd: string;
  };
};

if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
  passport.use(
    new GoogleStrategy(
      {
        clientID: env.GOOGLE_CLIENT_ID,
        clientSecret: env.GOOGLE_CLIENT_SECRET,
        callbackURL: `${env.URL}/auth/google.callback`,
        passReqToCallback: true,
        // @ts-expect-error StateStore
        store: new StateStore(),
        scope: scopes,
      },
      async function (
        req: Request,
        accessToken: string,
        refreshToken: string,
        params: { expires_in: number },
        profile: GoogleProfile,
        done: (
          err: Error | null,
          user: User | null,
          result?: AccountProvisionerResult
        ) => void
      ) {
        try {
          const state = req.cookies.get("state");
          const host = state ? parseState(state).host : req.hostname;
          // appDomain is the domain the user originated from when attempting auth
          const appDomain = parseDomain(host);

          let result;
          const domain = profile._json.hd;

          // Existence of domain means this is a Google Workspaces account
          // so we'll attempt to provision an account (team and user)
          if (domain) {
            const subdomain = domain.split(".")[0];
            const teamName = capitalize(subdomain);

            // Request a larger size profile picture than the default by tweaking
            // the query parameter.
            const avatarUrl = profile.picture.replace("=s96-c", "=s128-c");

            result = await accountProvisioner({
              ip: req.ip,
              team: {
                name: teamName,
                domain,
                subdomain,
              },
              user: {
                email: profile.email,
                name: profile.displayName,
                avatarUrl,
              },
              authenticationProvider: {
                name: providerName,
                providerId: domain,
              },
              authentication: {
                providerId: profile.id,
                accessToken,
                refreshToken,
                expiresIn: params.expires_in,
                scopes,
              },
            });
          } else {
            // No domain means it's a personal Gmail account
            // We only allow sign-in to existing user accounts

            let team;
            if (appDomain.custom) {
              team = await Team.findOne({ where: { domain: appDomain.host } });
            } else if (env.SUBDOMAINS_ENABLED && appDomain.teamSubdomain) {
              team = await Team.findOne({
                where: { subdomain: appDomain.teamSubdomain },
              });
            } else if (env.DEPLOYMENT !== "hosted") {
              team = await Team.findOne();
            }

            if (!team) {
              // No team usually means this is the apex domain
              // Throw different errors depending on whether we think the user is
              // trying to create a new account, or log-in to an existing one
              const userExists = await User.count({
                where: { email: profile.email.toLowerCase() },
              });

              if (!userExists) {
                throw GmailAccountCreationError();
              }

              throw TeamDomainRequiredError();
            }

            const user = await User.findOne({
              where: { teamId: team.id, email: profile.email.toLowerCase() },
            });

            if (!user) {
              throw InviteRequiredError();
            }

            result = {
              user,
              team,
              isNewUser: false,
              isNewTeam: false,
            };
          }

          return done(null, result.user, result);
        } catch (err) {
          return done(err, null);
        }
      }
    )
  );

  router.get(
    "google",
    passport.authenticate(providerName, {
      accessType: "offline",
      prompt: "select_account consent",
    })
  );

  router.get("google.callback", passportMiddleware(providerName));
}

export default router;
