import {auth} from '@googleapis/oauth2';
import {ApiError} from 'app/common/ApiError';
import {parseSubdomain} from 'app/common/gristUrls';
import {expressWrap} from 'app/server/lib/expressWrap';
import * as log from 'app/server/lib/log';
import {getOriginUrl, optStringParam, stringParam} from 'app/server/lib/requestUtils';
import * as express from 'express';
import {URL} from 'url';

/**
 * Google Auth Endpoint for performing server side authentication. More information can be found
 * at https://developers.google.com/identity/protocols/oauth2/web-server.
 *
 * Environmental variables used:
 * - GOOGLE_CLIENT_ID :     key obtained from a Google Project, not secret can be shared publicly,
 *                          the same client id is used in Google Drive Plugin
 * - GOOGLE_CLIENT_SECRET:  secret key for Google Project, can't be shared - it is used to (d)encrypt
 *                          data that we obtain from Google Auth Service (all done in the api)
 * - GOOGLE_DRIVE_SCOPE:    scope requested for the Google drive integration (defaults to drive.file which allows
 *                          to create files and get files via Google Drive Picker)
 *
 * High level description:
 *
 * Each API endpoint that wants to talk to Google Api needs an access_token that identifies our application
 * and permits us to access some of the user data or work with the API on the user's behalf (examples for that are
 * Google Drive plugin and Google Export endpoint, [Send to Google Drive] feature on the UI).)
 * To obtain this token on the server-side, the application needs to redirect the user to a
 * Google Consent Screen - where the user can log into his account and give consent for the permissions
 * we need. Permissions are defined by SCOPES - that exactly describes what we are allowed to do.
 * More on scopes can be read on https://developers.google.com/identity/protocols/oauth2/scopes.
 * When we are redirecting the user to a Google Consent Screen, we are also sending a static URL for an endpoint
 * where Google will redirect the user after he gives us permissions or declines our request. For that, we are exposing
 * static URL http://docs.getgrist.com/auth/google (on prod) or http://localhost:8080/auth/google (dev).
 *
 * NOTE: Actually, we are exposing only auth/google endpoint that can be accessed in various ways, including any
 * subdomain, but Google will always redirect to the configured endpoint (example: http://docs.getgrist.com/auth/google)
 *
 * This endpoint will render a simple page (see /static/message.html) that will immediately post
 * a message to the parent window with a response from Google in the form of { code? : string, error?: string }.
 * Code returned from Google will be an encrypted access_token that the client-side code should use to invoke
 * the server-side API endpoint that wishes to call one of the Google API endpoints. A server-side endpoint can use
 * "googleAuthTokenMiddleware" middleware to convert code to access_token (by making a separate call to Google
 * for exchanging code for an access_token).
 * This access_token could be stored in the user's session for further use, but since we are making only a single call
 * very rarely, and access_token will expire eventually; it is better to acquire access_token every time.
 * More on storing access_token offline can be read on:
 * https://developers.google.com/identity/protocols/oauth2/web-server#obtainingaccesstokens
 *
 * How to use:
 *
 * To call server-side endpoint that expects access_token, first decorate it with "googleAuthTokenMiddleware"
 * middleware, then perform "server-side" authentication with Google on the client-side to acquire an encrypted token.
 * Client code should open up a popup window with an URL to Grist's Google Auth endpoint (/auth/google) and wait
 * for a message from a popup window containing an encrypted token.
 * Having encrypted token ("code"), a client can call the server-side endpoint by adding to the query string the code
 * acquired from the popup window. Server endpoint (decorated by "googleAuthTokenMiddleware") will get access_token
 * in a query string.
 */

// Path for the auth endpoint.
const authHandlerPath = "/auth/google";

// Redirect host after the Google Auth login form is completed. This reuses the same domain name
// as for Cognito login.
const AUTH_SUBDOMAIN = process.env.GRIST_ID_PREFIX ? `docs-${process.env.GRIST_ID_PREFIX}` : 'docs';

/**
 * Return a full url for Google Auth handler. Examples are:
 *
 * http://localhost:8080/auth/google in dev
 * https://docs-s.getgrist.com in staging
 * https://docs.getgrist.com in prod
 */
function getFullAuthEndpointUrl(): string {
  const homeUrl = process.env.APP_HOME_URL;
  // if homeUrl is localhost - (in dev environment) - use the development url
  if (homeUrl && new URL(homeUrl).hostname === "localhost") {
    return `${homeUrl}${authHandlerPath}`;
  }
  const homeBaseDomain = homeUrl && parseSubdomain(new URL(homeUrl).host).base;
  const baseDomain = homeBaseDomain || '.getgrist.com';
  return `https://${AUTH_SUBDOMAIN}${baseDomain}${authHandlerPath}`;
}

/**
 * Middleware for obtaining access_token from Google Auth Service.
 * It expects code query parameter (provided by frontend code) add adds to access_token to query parameters.
 */
export async function googleAuthTokenMiddleware(
  req: express.Request,
  res: express.Response,
  next: express.NextFunction) {
  // If access token is in place, proceed
  if (!optStringParam(req.query.code)) {
    throw new ApiError("Google Auth endpoint requires a code parameter in the query string", 400);
  } else {
    try {
      const oAuth2Client = getGoogleAuth();
      // Decrypt code that was send back from Google Auth service. Uses GOOGLE_CLIENT_SECRET key.
      const tokenResponse = await oAuth2Client.getToken(stringParam(req.query.code));
      // Get the access token (access token will be present in a default request configuration).
      const access_token = tokenResponse.tokens.access_token!;
      req.query.access_token = access_token;
      next();
    } catch (err) {
      log.error("GoogleAuth - Error", err);
      throw err;
    }
  }
}

/**
 * Adds a static Google Auth endpoint. This will be used by Google Auth Service to redirect back, after the user
 * finishes a signing and a consent flow. Google will pass 2 arguments in a query string:
 * - code: encrypted access token when user gave permissions
 * - error: error code when user declined our request.
 */
export function addGoogleAuthEndpoint(
  expressApp: express.Application,
  messagePage: (req: express.Request, res: express.Response, message: any) => any
) {
  if (!process.env.GOOGLE_CLIENT_SECRET) {
    log.warn("Failed to create GoogleAuth endpoint: GOOGLE_CLIENT_SECRET is not defined");
    expressApp.get(authHandlerPath, expressWrap(async (req: express.Request, res: express.Response) => {
      throw new Error("Send to Google Drive is not configured.");
    }));
    return;
  }
  log.info(`GoogleAuth - auth handler at ${getFullAuthEndpointUrl()}`);

  expressApp.get(authHandlerPath, expressWrap(async (req: express.Request, res: express.Response) => {

    // Test if the code is in a query string. Google sends it back after user has given a concent for
    // our request. It is encrypted (with CLIENT_SECRET) and signed with redirect url.
    // In state query parameter we will receive an url that was send as part of the request to Google.

    if (optStringParam(req.query.code)) {
      log.debug("GoogleAuth - response from Google with valid code");
      messagePage(req, res, { code: stringParam(req.query.code),
                              origin: stringParam(req.query.state) });
    } else if (optStringParam(req.query.error)) {
      log.debug("GoogleAuth - response from Google with error code", stringParam(req.query.error));
      if (stringParam(req.query.error) === "access_denied") {
        messagePage(req, res, { error: stringParam(req.query.error),
                                origin: stringParam(req.query.state) });
      } else {
        // This should not happen, either code or error is a mandatory query parameter.
        throw new ApiError("Error authenticating with Google", 500);
      }
    } else {
      const oAuth2Client = getGoogleAuth();
      const scope = stringParam(req.query.scope);
      // Create url for origin parameter for a popup window.
      const origin = getOriginUrl(req);
      const authUrl = oAuth2Client.generateAuthUrl({
        scope,
        prompt: 'select_account',
        state: origin
      });
      log.debug(`GoogleAuth - redirecting to Google consent screen`, {
        authUrl,
        scope,
        state: origin
      });
      res.redirect(authUrl);
    }
  }));
}

/**
 * Builds the OAuth2 Google client.
 */
export function getGoogleAuth() {
  const CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
  const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
  const oAuth2Client = new auth.OAuth2(CLIENT_ID, CLIENT_SECRET, getFullAuthEndpointUrl());
  return oAuth2Client;
}
