import { randomString } from '../cryptoUtils';
import { inflate } from '../triggers';
import AdaptableController from './AdaptableController';
import MailAdapter from '../Adapters/Email/MailAdapter';
import rest from '../rest';
import Parse from 'parse/node';
import AccountLockout from '../AccountLockout';
import Config from '../Config';

var RestQuery = require('../RestQuery');
var Auth = require('../Auth');

export class UserController extends AdaptableController {
  constructor(adapter, appId, options = {}) {
    super(adapter, appId, options);
  }

  get config() {
    return Config.get(this.appId);
  }

  validateAdapter(adapter) {
    // Allow no adapter
    if (!adapter && !this.shouldVerifyEmails) {
      return;
    }
    super.validateAdapter(adapter);
  }

  expectedAdapterType() {
    return MailAdapter;
  }

  get shouldVerifyEmails() {
    return (this.config || this.options).verifyUserEmails;
  }

  async setEmailVerifyToken(user, req, storage = {}) {
    const shouldSendEmail =
      this.shouldVerifyEmails === true ||
      (typeof this.shouldVerifyEmails === 'function' &&
        (await Promise.resolve(this.shouldVerifyEmails(req))) === true);
    if (!shouldSendEmail) {
      return false;
    }
    storage.sendVerificationEmail = true;
    user._email_verify_token = randomString(25);
    if (
      !storage.fieldsChangedByTrigger ||
      !storage.fieldsChangedByTrigger.includes('emailVerified')
    ) {
      user.emailVerified = false;
    }

    if (this.config.emailVerifyTokenValidityDuration) {
      user._email_verify_token_expires_at = Parse._encode(
        this.config.generateEmailVerifyTokenExpiresAt()
      );
    }
    return true;
  }

  async verifyEmail(username, token) {
    if (!this.shouldVerifyEmails) {
      // Trying to verify email when not enabled
      // TODO: Better error here.
      throw undefined;
    }

    const query = { username: username, _email_verify_token: token };
    const updateFields = {
      emailVerified: true,
      _email_verify_token: { __op: 'Delete' },
    };

    // if the email verify token needs to be validated then
    // add additional query params and additional fields that need to be updated
    if (this.config.emailVerifyTokenValidityDuration) {
      query.emailVerified = false;
      query._email_verify_token_expires_at = { $gt: Parse._encode(new Date()) };

      updateFields._email_verify_token_expires_at = { __op: 'Delete' };
    }
    const maintenanceAuth = Auth.maintenance(this.config);
    var findUserForEmailVerification = await RestQuery({
      method: RestQuery.Method.get,
      config: this.config,
      auth: maintenanceAuth,
      className: '_User',
      restWhere: {
        username,
      },
    });
    return findUserForEmailVerification.execute().then(result => {
      if (result.results.length && result.results[0].emailVerified) {
        return Promise.resolve(result.results.length[0]);
      } else if (result.results.length) {
        query.objectId = result.results[0].objectId;
      }
      return rest.update(this.config, maintenanceAuth, '_User', query, updateFields);
    });
  }

  checkResetTokenValidity(username, token) {
    return this.config.database
      .find(
        '_User',
        {
          username: username,
          _perishable_token: token,
        },
        { limit: 1 },
        Auth.maintenance(this.config)
      )
      .then(results => {
        if (results.length != 1) {
          throw 'Failed to reset password: username / email / token is invalid';
        }

        if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) {
          let expiresDate = results[0]._perishable_token_expires_at;
          if (expiresDate && expiresDate.__type == 'Date') {
            expiresDate = new Date(expiresDate.iso);
          }
          if (expiresDate < new Date()) { throw 'The password reset link has expired'; }
        }
        return results[0];
      });
  }

  async getUserIfNeeded(user) {
    var where = {};
    if (user.username) {
      where.username = user.username;
    }
    if (user.email) {
      where.email = user.email;
    }

    var query = await RestQuery({
      method: RestQuery.Method.get,
      config: this.config,
      runBeforeFind: false,
      auth: Auth.master(this.config),
      className: '_User',
      restWhere: where,
    });
    const result = await query.execute();
    if (result.results.length != 1) {
      throw undefined;
    }
    return result.results[0];
  }

  async sendVerificationEmail(user, req) {
    if (!this.shouldVerifyEmails) {
      return;
    }
    const token = encodeURIComponent(user._email_verify_token);
    // We may need to fetch the user in case of update email; only use the `fetchedUser`
    // from this point onwards; do not use the `user` as it may not contain all fields.
    const fetchedUser = await this.getUserIfNeeded(user);
    let shouldSendEmail = this.config.sendUserEmailVerification;
    if (typeof shouldSendEmail === 'function') {
      const response = await Promise.resolve(
        this.config.sendUserEmailVerification({
          user: Parse.Object.fromJSON({ className: '_User', ...fetchedUser }),
          master: req.auth?.isMaster,
        })
      );
      shouldSendEmail = !!response;
    }
    if (!shouldSendEmail) {
      return;
    }
    const username = encodeURIComponent(fetchedUser.username);

    const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config);
    const options = {
      appName: this.config.appName,
      link: link,
      user: inflate('_User', fetchedUser),
    };
    if (this.adapter.sendVerificationEmail) {
      this.adapter.sendVerificationEmail(options);
    } else {
      this.adapter.sendMail(this.defaultVerificationEmail(options));
    }
  }

  /**
   * Regenerates the given user's email verification token
   *
   * @param user
   * @returns {*}
   */
  async regenerateEmailVerifyToken(user, master, installationId, ip) {
    const { _email_verify_token } = user;
    let { _email_verify_token_expires_at } = user;
    if (_email_verify_token_expires_at && _email_verify_token_expires_at.__type === 'Date') {
      _email_verify_token_expires_at = _email_verify_token_expires_at.iso;
    }
    if (
      this.config.emailVerifyTokenReuseIfValid &&
      this.config.emailVerifyTokenValidityDuration &&
      _email_verify_token &&
      new Date() < new Date(_email_verify_token_expires_at)
    ) {
      return Promise.resolve(true);
    }
    const shouldSend = await this.setEmailVerifyToken(user, {
      object: Parse.User.fromJSON(Object.assign({ className: '_User' }, user)),
      master,
      installationId,
      ip,
      resendRequest: true
    });
    if (!shouldSend) {
      return;
    }
    return this.config.database.update('_User', { username: user.username }, user);
  }

  async resendVerificationEmail(username, req) {
    const aUser = await this.getUserIfNeeded({ username: username });
    if (!aUser || aUser.emailVerified) {
      throw undefined;
    }
    const generate = await this.regenerateEmailVerifyToken(aUser, req.auth?.isMaster, req.auth?.installationId, req.ip);
    if (generate) {
      this.sendVerificationEmail(aUser, req);
    }
  }

  setPasswordResetToken(email) {
    const token = { _perishable_token: randomString(25) };

    if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) {
      token._perishable_token_expires_at = Parse._encode(
        this.config.generatePasswordResetTokenExpiresAt()
      );
    }

    return this.config.database.update(
      '_User',
      { $or: [{ email }, { username: email, email: { $exists: false } }] },
      token,
      {},
      true
    );
  }

  async sendPasswordResetEmail(email) {
    if (!this.adapter) {
      throw 'Trying to send a reset password but no adapter is set';
      //  TODO: No adapter?
    }
    let user;
    if (
      this.config.passwordPolicy &&
      this.config.passwordPolicy.resetTokenReuseIfValid &&
      this.config.passwordPolicy.resetTokenValidityDuration
    ) {
      const results = await this.config.database.find(
        '_User',
        {
          $or: [
            { email, _perishable_token: { $exists: true } },
            { username: email, email: { $exists: false }, _perishable_token: { $exists: true } },
          ],
        },
        { limit: 1 },
        Auth.maintenance(this.config)
      );
      if (results.length == 1) {
        let expiresDate = results[0]._perishable_token_expires_at;
        if (expiresDate && expiresDate.__type == 'Date') {
          expiresDate = new Date(expiresDate.iso);
        }
        if (expiresDate > new Date()) {
          user = results[0];
        }
      }
    }
    if (!user || !user._perishable_token) {
      user = await this.setPasswordResetToken(email);
    }
    const token = encodeURIComponent(user._perishable_token);
    const username = encodeURIComponent(user.username);

    const link = buildEmailLink(this.config.requestResetPasswordURL, username, token, this.config);
    const options = {
      appName: this.config.appName,
      link: link,
      user: inflate('_User', user),
    };

    if (this.adapter.sendPasswordResetEmail) {
      this.adapter.sendPasswordResetEmail(options);
    } else {
      this.adapter.sendMail(this.defaultResetPasswordEmail(options));
    }

    return Promise.resolve(user);
  }

  updatePassword(username, token, password) {
    return this.checkResetTokenValidity(username, token)
      .then(user => updateUserPassword(user, password, this.config))
      .then(user => {
        const accountLockoutPolicy = new AccountLockout(user, this.config);
        return accountLockoutPolicy.unlockAccount();
      })
      .catch(error => {
        if (error && error.message) {
          // in case of Parse.Error, fail with the error message only
          return Promise.reject(error.message);
        } else {
          return Promise.reject(error);
        }
      });
  }

  defaultVerificationEmail({ link, user, appName }) {
    const text =
      'Hi,\n\n' +
      'You are being asked to confirm the e-mail address ' +
      user.get('email') +
      ' with ' +
      appName +
      '\n\n' +
      '' +
      'Click here to confirm it:\n' +
      link;
    const to = user.get('email');
    const subject = 'Please verify your e-mail for ' + appName;
    return { text, to, subject };
  }

  defaultResetPasswordEmail({ link, user, appName }) {
    const text =
      'Hi,\n\n' +
      'You requested to reset your password for ' +
      appName +
      (user.get('username') ? " (your username is '" + user.get('username') + "')" : '') +
      '.\n\n' +
      '' +
      'Click here to reset it:\n' +
      link;
    const to = user.get('email') || user.get('username');
    const subject = 'Password Reset for ' + appName;
    return { text, to, subject };
  }
}

// Mark this private
function updateUserPassword(user, password, config) {
  return rest
    .update(
      config,
      Auth.master(config),
      '_User',
      { objectId: user.objectId },
      {
        password: password,
      }
    )
    .then(() => user);
}

function buildEmailLink(destination, username, token, config) {
  const usernameAndToken = `token=${token}&username=${username}`;

  if (config.parseFrameURL) {
    const destinationWithoutHost = destination.replace(config.publicServerURL, '');

    return `${config.parseFrameURL}?link=${encodeURIComponent(
      destinationWithoutHost
    )}&${usernameAndToken}`;
  } else {
    return `${destination}?${usernameAndToken}`;
  }
}

export default UserController;
