'use strict'

const AuthStrategy = require('@pm2/js-api/src/auth_strategies/strategy')
const http = require('http')
const fs = require('fs')
const exec = require('child_process').exec
const tryEach = require('async/tryEach')
const path = require('path')
const os = require('os')
const chalk = require('ansis')
const cst = require('../../../../constants.js')
const promptly = require('../../../tools/prompt')

module.exports = class CliStrategy extends AuthStrategy {
  // the client will try to call this but we handle this part ourselves
  retrieveTokens (km, cb) {
    this.authenticated = false
    this.callback = cb
    this.km = km
    this.BASE_URI = 'https://id.keymetrics.io';
  }

  // so the cli know if we need to tell user to login/register
  isAuthenticated () {
    return new Promise((resolve, reject) => {
      if (this.authenticated) return resolve(true)

      let tokensPath = cst.PM2_IO_ACCESS_TOKEN
      fs.readFile(tokensPath, (err, tokens) => {
        if (err && err.code === 'ENOENT') return resolve(false)
        if (err) return reject(err)

        // verify that the token is valid
        try {
          tokens = JSON.parse(tokens || '{}')
        } catch (err) {
          fs.unlinkSync(tokensPath)
          return resolve(false)
        }

        // if the refresh tokens is here, the user could be automatically authenticated
        return resolve(typeof tokens.refresh_token === 'string')
      })
    })
  }

  verifyToken (refresh) {
    return this.km.auth.retrieveToken({
      client_id: this.client_id,
      refresh_token: refresh
    })
  }

  // called when we are sure the user asked to be logged in
  _retrieveTokens (optionalCallback) {
    const km = this.km
    const cb = this.callback

    tryEach([
      // try to find the token via the environment
      (next) => {
        if (!process.env.PM2_IO_TOKEN) {
          return next(new Error('No token in env'))
        }
        this.verifyToken(process.env.PM2_IO_TOKEN)
          .then((res) => {
            return next(null, res.data)
          }).catch(next)
      },
      // try to find it in the file system
      (next) => {
        fs.readFile(cst.PM2_IO_ACCESS_TOKEN, (err, tokens) => {
          if (err) return next(err)
          // verify that the token is valid
          tokens = JSON.parse(tokens || '{}')
          if (new Date(tokens.expire_at) > new Date(new Date().toISOString())) {
            return next(null, tokens)
          }

          this.verifyToken(tokens.refresh_token)
            .then((res) => {
              return next(null, res.data)
            }).catch(next)
        })
      },
      // otherwise make the whole flow
      (next) => {
        return this.authenticate((err, data) => {
          if (err instanceof Error) return next(err)
          // verify that the token is valid
          this.verifyToken(data.refresh_token)
            .then((res) => {
              return next(null, res.data)
            }).catch(next)
        })
      }
    ], (err, result) => {
      // if present run the optional callback
      if (typeof optionalCallback === 'function') {
        optionalCallback(err, result)
      }

      if (result.refresh_token) {
        this.authenticated = true
        let file = cst.PM2_IO_ACCESS_TOKEN
        fs.writeFile(file, JSON.stringify(result), () => {
          return cb(err, result)
        })
      } else {
        return cb(err, result)
      }
    })
  }

  authenticate (cb) {
    console.log(`${cst.PM2_IO_MSG} Using non-browser authentication.`)
    promptly.confirm(`${cst.PM2_IO_MSG} Do you have a pm2.io account? (y/n)`, (err, answer) => {
      // Either login or register
      return answer === true ? this.login(cb) : this.register(cb)
    })
  }

  login (cb) {
    let retry = () => {
      promptly.prompt(`${cst.PM2_IO_MSG} Your username or email: `, (err, username) => {
        if (err) return retry();

        promptly.password(`${cst.PM2_IO_MSG} Your password: `, { replace : '*' }, (err, password) => {
          if (err) return retry();

          console.log(`${cst.PM2_IO_MSG} Authenticating ...`)
          this._loginUser({
            username: username,
            password: password
          }, (err, data) => {
            if (err) {
              console.error(`${cst.PM2_IO_MSG_ERR} Failed to authenticate: ${err.message}`)
              return retry()
            }
            return cb(null, data)
          })
        })
      })
    }

    retry()
  }

  register (cb) {
    console.log(`${cst.PM2_IO_MSG} No problem ! We just need few informations to create your account`)

    var retry = () => {
      promptly.prompt(`${cst.PM2_IO_MSG} Please choose an username :`, {
        validator : this._validateUsername,
        retry : true
      }, (err, username) => {
        promptly.prompt(`${cst.PM2_IO_MSG} Please choose an email :`, {
          validator : this._validateEmail,
          retry : true
        },(err, email) => {
          promptly.password(`${cst.PM2_IO_MSG} Please choose a password :`, { replace : '*' }, (err, password) => {
            promptly.confirm(`${cst.PM2_IO_MSG} Do you accept the terms and privacy policy (https://pm2.io/legals/terms_conditions.pdf) ?  (y/n)`, (err, answer) => {
              if (err) {
                console.error(chalk.bold.red(err));
                return retry()
              } else if (answer === false) {
                console.error(`${cst.PM2_IO_MSG_ERR} You must accept the terms and privacy policy to contiue.`)
                return retry()
              }

              this._registerUser({
                email : email,
                password : password,
                username : username
              }, (err, data) => {
                console.log('\n')
                if (err) {
                  console.error(`${cst.PM2_IO_MSG_ERR} Unexpect error: ${err.message}`)
                  console.error(`${cst.PM2_IO_MSG_ERR} You can also contact us to get help: contact@pm2.io`)
                  return process.exit(1)
                }
                return cb(undefined, data)
              })
            })
          })
        })
      })
    }
    retry()
  }

  /**
   * Register function
   * @param opts.username
   * @param opts.password
   * @param opts.email
   */
  _registerUser (opts, cb) {
    const data = Object.assign(opts, {
      password_confirmation: opts.password,
      accept_terms: true
    })
    fetch(this.BASE_URI + '/api/oauth/register', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Register-Provider': 'pm2-register',
        'x-client-id': this.client_id
      },
      body: JSON.stringify(data)
    })
    .then(res => res.json())
    .then(body => {
      if (body.email && body.email.message) return cb(new Error(body.email.message))
      if (body.username && body.username.message) return cb(new Error(body.username.message))
      if (!body.access_token) return cb(new Error(body.msg))
      return cb(null, {
        refresh_token : body.refresh_token.token,
        access_token : body.access_token.token
      })
    })
    .catch(err => cb(err));
  }

  _loginUser (user_info, cb) {
    const URL_AUTH = '/api/oauth/authorize?response_type=token&scope=all&client_id=' +
            this.client_id + '&redirect_uri=http://localhost:43532';

    var extractCookies = (res) => {
      return (res.headers.getSetCookie() || [])
        .map(c => c.split(';')[0]).join('; ');
    };

    fetch(this.BASE_URI + URL_AUTH, { redirect: 'manual' })
      .then(res => {
        var cookie = extractCookies(res);

        return fetch(this.BASE_URI + '/api/oauth/login', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Cookie': cookie
          },
          body: JSON.stringify(user_info),
          redirect: 'manual'
        }).then(resp => {
          if (resp.status != 200 && resp.status != 302) return cb(new Error('Wrong credentials'))

          var location = resp.headers.get('x-redirect')
          if (!location) return cb(new Error('Missing redirect header from server'))

          return fetch(this.BASE_URI + location, {
            headers: { 'Cookie': cookie },
            redirect: 'manual'
          }).then(res => {
            var redirectLocation = res.headers.get('location')
            if (!redirectLocation) return cb(new Error('Missing location header from server'))
            var parsedLocation = new URL(redirectLocation, this.BASE_URI);
            var refresh_token = parsedLocation.searchParams.get('access_token');

            return fetch(this.BASE_URI + '/api/oauth/token', {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
                'Cookie': cookie
              },
              body: JSON.stringify({
                client_id : this.client_id,
                grant_type : 'refresh_token',
                refresh_token : refresh_token,
                scope : 'all'
              })
            }).then(res => res.json()).then(body => cb(null, body));
          });
        });
      })
      .catch(err => cb(err));
  }

  _validateEmail (email) {
    var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
    if (re.test(email) == false)
      throw new Error('Not an email');
    return email;
  }

  _validateUsername (value) {
    if (value.length < 6) {
      throw new Error('Min length of 6');
    }
    return value;
  };

  deleteTokens (km) {
    return new Promise((resolve, reject) => {
      // revoke the refreshToken
      km.auth.revoke()
        .then(res => {
          // remove the token from the filesystem
          let file = cst.PM2_IO_ACCESS_TOKEN
          fs.unlinkSync(file)
          return resolve(res)
        }).catch(reject)
    })
  }
}
