/*global process */
/**
 * @file Command line interface for JSforce
 * @author Shinichi Tomita <shinichi.tomita@gmail.com>
 */

'use strict';

var http = require('http'),
    url = require('url'),
    crypto = require('crypto'),
    openUrl = require('opn'),
    commander = require('commander'),
    coprompt = require('co-prompt'),
    request = require('request'),
    base64url = require('base64-url'),
    Repl = require('./repl'),
    jsforce = require('../jsforce'),
    registry = jsforce.registry,
    Promise = require('../promise'),
    pkg = require('../../package.json');

var repl;
var conn = null;
var connName = null;
var outputEnabled = true;
var defaultLoginUrl = null;

/**
 * @private
 */
function start() {
  var program = new commander.Command();
  program.option('-u, --username [username]', 'Salesforce username')
         .option('-p, --password [password]', 'Salesforce password (and security token, if available)')
         .option('-c, --connection [connection]', 'Connection name stored in connection registry')
         .option('-e, --evalScript [evalScript]', 'Script to evaluate')
         .option('-l, --loginUrl [loginUrl]', 'Salesforce login url')
         .option('--sandbox', 'Login to Salesforce sandbox')
         .option('--coffee', 'Using CoffeeScript')
         .version(pkg.version)
         .parse(process.argv);
  var replModule = program.coffee ? require('coffee-script/lib/coffee-script/repl') : require('repl');
  repl = new Repl(cli, replModule);
  outputEnabled = !program.evalScript;
  var options = { username: program.username, password: program.password };
  var loginUrl = program.loginUrl ? program.loginUrl :
                 program.sandbox ? 'sandbox' :
                 null;
  setLoginServer(loginUrl);
  connect(program.connection, options, function(err, res) {
    if (err) {
      console.error(err.message);
      process.exit();
    } else {
      if (program.evalScript) {
        repl.start({
          interactive: false,
          evalScript: program.evalScript
        });
      } else {
        repl.start();
      }
    }
  });
}

/**
 * @private
 */
function getCurrentConnection() {
  return conn;
}

function print(message) {
  if (outputEnabled) { console.log(message); }
}

/**
 * @private
 */
function saveCurrentConnection() {
  if (conn && connName) {
    var connConfig = {
      oauth2: conn.oauth2 && {
        clientId: conn.oauth2.clientId,
        clientSecret: conn.oauth2.clientSecret,
        redirectUri: conn.oauth2.redirectUri,
        loginUrl: conn.oauth2.loginUrl
      },
      accessToken: conn.accessToken,
      instanceUrl: conn.instanceUrl,
      refreshToken: conn.refreshToken
    };
    registry.saveConnectionConfig(connName, connConfig);
  }
}

/**
 * @private
 */
function setLoginServer(loginServer) {
  if (!loginServer) { return; }
  if (loginServer === 'production') {
    defaultLoginUrl = 'https://login.salesforce.com';
  } else if (loginServer === 'sandbox') {
    defaultLoginUrl = 'https://test.salesforce.com';
  } else if (loginServer.indexOf('https://') !== 0) {
    defaultLoginUrl = 'https://' + loginServer;
  } else {
    defaultLoginUrl = loginServer;
  }
  print('Using "' + defaultLoginUrl + '" as default login URL.');
}

/**
 * @private
 */
function connect(name, options, callback) {
  connName = name;
  options = options || {};
  var connConfig = registry.getConnectionConfig(name);
  var username, password;
  if (!connConfig) {
    connConfig = {};
    if (defaultLoginUrl) {
      connConfig.loginUrl = defaultLoginUrl;
    }
    username = name;
  }
  conn = new jsforce.Connection(connConfig);
  username = username || options.username;
  password = options.password;
  var handleLogin = function(err) {
    if (err) { return callback(err); }
    saveCurrentConnection();
    callback();
  };
  if (username) {
    loginByPassword(username, password, 2, handleLogin);
  } else {
    if (connName && conn.accessToken) {
      conn.on('refresh', function(accessToken) {
        print('Refreshing access token ... ');
        saveCurrentConnection();
      });
      conn.identity(function(err, identity) {
        if (err) {
          print(err.message);
          if (conn.oauth2) {
            callback(new Error('Please re-authorize connection.'));
          } else {
            loginByPassword(connName, null, 2, handleLogin);
          }
        } else {
          print('Logged in as : ' + identity.username);
          callback();
        }
      });
    } else {
      callback();
    }
  }
}

/**
 * @private
 */
function loginByPassword(username, password, retry, callback) {
  if (!password) {
    promptPassword('Password: ', function(err, pass) {
      if (err) { return callback(err); }
      loginByPassword(username, pass, retry, callback);
    });
    return;
  }
  conn.login(username, password, function(err, result) {
    if (err) {
      console.error(err.message);
      if (retry > 0) {
        loginByPassword(username, null, --retry, callback);
      } else {
        callback(new Error());
      }
    } else {
      print("Logged in as : " + username);
      callback(null, result);
    }
  });
}

/**
 * @private
 */
function disconnect(name) {
  name = name || connName;
  if (registry.getConnectionConfig(name)) {
    registry.removeConnectionConfig(name);
    print("Disconnect connection '" + name + "'");
  }
  connName = null;
  conn = new jsforce.Connection();
}

/**
 * @private
 */
function authorize(clientName, callback) {
  clientName = clientName || 'default';
  var oauth2Config = registry.getClient(clientName);
  if (!oauth2Config || !oauth2Config.clientId) {
    if (clientName === 'default' || clientName === 'sandbox') {
      print('No client information registered. Downloading JSforce default client information...');
      return downloadDefaultClientInfo(clientName, callback);
    }
    return callback(new Error("No OAuth2 client information registered : '"+clientName+"'. Please register client info first."));
  }
  var oauth2 = new jsforce.OAuth2(oauth2Config);
  var verifier = base64url.encode(crypto.randomBytes(32));
  var challenge = base64url.encode(crypto.createHash('sha256').update(verifier).digest());
  var state = base64url.encode(crypto.randomBytes(32));
  var authzUrl = oauth2.getAuthorizationUrl({ code_challenge: challenge, state: state });
  print('Opening authorization page in browser...');
  print('URL: ' + authzUrl);
  openUrl(authzUrl);
  waitCallback(oauth2Config.redirectUri, state, function(err, params) {
    if (err) { return callback(err); }
    conn = new jsforce.Connection({ oauth2: oauth2 });
    if (!params.code) {
      return callback(new Error('No authorization code returned.'));
    }
    if (params.state !== state) {
      return callback(new Error('Invalid state parameter returned.'));
    }
    print('Received authorization code. Please close the opened browser window.');
    conn.authorize(params.code, { code_verifier: verifier }).then(function(res) {
      print('Authorized. Fetching user info...');
      return conn.identity();
    }).then(function(identity) {
      print('Logged in as : ' + identity.username);
      connName = identity.username;
      saveCurrentConnection();
    }).thenCall(callback);
  });
}

/**
 * @private
 */
function downloadDefaultClientInfo(clientName, callback) {
  var configUrl = 'https://jsforce.github.io/client-config/default.json';
  request(configUrl, function(err, res) {
    if (err) { return callback(err); }
    var clientConfig = JSON.parse(res.body);
    if (clientName === 'sandbox') {
      clientConfig.loginUrl = 'https://test.salesforce.com';
    }
    registry.registerClient(clientName, clientConfig);
    print("Client information downloaded successfully.");
    authorize(clientName, callback);
  });
}


/**
 * @private
 */
function waitCallback(serverUrl, state, callback) {
  if (serverUrl.indexOf('http://localhost:') === 0) {
    var server = http.createServer(function(req, res) {
      var qparams = url.parse(req.url, true).query;
      res.writeHead(200, {'Content-Type': 'text/html'});
      res.write('<html><script>location.href="about:blank";</script></html>');
      res.end();
      callback(null, qparams);
      server.close();
      req.connection.end();
      req.connection.destroy();
    });
    var port = url.parse(serverUrl).port;
    server.listen(port, "localhost");
  } else {
    var msg = 'Copy & paste authz code passed in redirected URL: ';
    promptMessage(msg, function(err, code) {
      if (err) {
        callback(err);
      } else {
        callback(null, { code: decodeURIComponent(code), state: state });
      }
    });
  }
}

/**
 * @private
 */
function register(clientName, clientConfig, callback) {
  if (!clientName) {
    clientName = "default";
  }

  var prompts = {
    "clientId": "Input client ID : ",
    "clientSecret": "Input client secret (optional) : ",
    "redirectUri": "Input redirect URI : ",
    "loginUrl": "Input login URL (default is https://login.salesforce.com) : "
  };
  new Promise(function(resolve, reject) {
    if (registry.getClient(clientName)) {
      var msg = "Client '"+clientName+"' is already registered. Are you sure you want to override ? [yN] : ";
      promptConfirm(msg, function(err, ok) {
        if (ok) {
          resolve();
        } else {
          reject(new Error('Registration canceled.'));
        }
      });
    } else {
      resolve();
    }
  })
  .then(function() {
    return Object.keys(prompts).reduce(function(promise, name) {
      return promise.then(function() {
        var message = prompts[name];
        return new Promise(function(resolve, reject) {
          if (!clientConfig[name]) {
            promptMessage(message, function(err, value) {
              if (err) { return reject(err); }
              if (value) { clientConfig[name] = value; }
              resolve();
            });
          } else {
            resolve();
          }
        });
      });
    }, Promise.resolve());
  }).then(function() {
    registry.registerClient(clientName, clientConfig);
    print("Client registered successfully.");
  }).thenCall(callback);
}

/**
 * @private
 */
function listConnections() {
  var names = registry.getConnectionNames();
  for (var i=0; i<names.length; i++) {
    var name = names[i];
    print((name === connName ? '* ' : '  ') + name);
  }
}

/**
 * @private
 */
function getConnectionNames() {
  return registry.getConnectionNames();
}

/**
 * @private
 */
function getClientNames() {
  return registry.getClientNames();
}

/**
 * @private
 */
function promptMessage(message, callback) {
  repl.pause();
  coprompt(message)(function(err, res) {
    repl.resume();
    callback(err, res);
  });
}

/**
 * @private
 */
function promptPassword(message, callback) {
  repl.pause();
  coprompt.password(message)(function(err, res) {
    repl.resume();
    callback(err, res);
  });
}

/**
 * @private
 */
function promptConfirm(message, callback) {
  repl.pause();
  coprompt.confirm(message)(function(err, res) {
    repl.resume();
    callback(err, res);
  });
}

/**
 * @private
 */
function openUrlUsingSession(url) {
  var frontdoorUrl = conn.instanceUrl + '/secur/frontdoor.jsp?sid=' + conn.accessToken;
  if (url) {
    frontdoorUrl += "&retURL=" + encodeURIComponent(url);
  }
  openUrl(frontdoorUrl);
}

/* ------------------------------------------------------------------------- */

var cli = {
  start: start,
  getCurrentConnection: getCurrentConnection,
  saveCurrentConnection: saveCurrentConnection,
  listConnections: listConnections,
  setLoginServer: setLoginServer,
  getConnectionNames: getConnectionNames,
  getClientNames: getClientNames,
  connect: connect,
  disconnect: disconnect,
  authorize: authorize,
  register: register,
  openUrl: openUrlUsingSession
};

module.exports = cli;
