"use strict";

/**
 * @file backend session service for backend session
 */
const utils = require('../../util/utils');

const EXPORTED_FIELDS = ['id', 'frontendId', 'uid', 'settings'];

/**
 * Service that maintains backend sessions and the communication with frontend
 * servers.
 *
 * BackendSessionService would be created in each server process and maintains
 * backend sessions for current process and communicates with the relative
 * frontend servers.
 *
 * BackendSessionService instance could be accessed by
 * `app.get('backendSessionService')` or app.backendSessionService.
 *
 * @class
 * @constructor
 */
let BackendSessionService = function(app) {
  this.app = app;
};

module.exports = BackendSessionService;

BackendSessionService.prototype.create = function(opts) {
  if(!opts) {
    throw new Error('opts should not be empty.');
  }
  return new BackendSession(opts, this);
};

/**
 * Get backend session by frontend server id and session id.
 *
 * @param  {String}   frontendId frontend server id that session attached
 * @param  {String}   sid        session id
 * @param  {Function} cb         callback function. args: cb(err, BackendSession)
 *
 * @memberOf BackendSessionService
 */
BackendSessionService.prototype.get = function(frontendId, sid, cb) {
  let namespace = 'sys';
  let service = 'sessionRemote';
  let method = 'getBackendSessionBySid';
  let args = [sid];
  rpcInvoke(this.app, frontendId, namespace, service, method,
            args, BackendSessionCB.bind(null, this, cb));
};

/**
 * Get backend sessions by frontend server id and user id.
 *
 * @param  {String}   frontendId frontend server id that session attached
 * @param  {String}   uid        user id binded with the session
 * @param  {Function} cb         callback function. args: cb(err, BackendSessions)
 *
 * @memberOf BackendSessionService
 */
BackendSessionService.prototype.getByUid = function(frontendId, uid, cb) {
  let namespace = 'sys';
  let service = 'sessionRemote';
  let method = 'getBackendSessionsByUid';
  let args = [uid];
  rpcInvoke(this.app, frontendId, namespace, service, method,
            args, BackendSessionCB.bind(null, this, cb));
};

/**
 * Kick a session by session id.
 *
 * @param  {String}   frontendId cooperating frontend server id
 * @param  {Number}   sid        session id
 * @param  {Function} cb         callback function
 *
 * @memberOf BackendSessionService
 */
BackendSessionService.prototype.kickBySid = function(frontendId, sid, reason, cb) {
  let namespace = 'sys';
  let service = 'sessionRemote';
  let method = 'kickBySid';
  let args = [sid];
  if(typeof reason === 'function') {
    cb = reason;
  }else{
    args.push(reason);
  }
  rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
};

/**
 * Kick sessions by user id.
 *
 * @param  {String}          frontendId cooperating frontend server id
 * @param  {Number|String}   uid        user id
 * @param  {String}          reason     kick reason
 * @param  {Function}        cb         callback function
 *
 * @memberOf BackendSessionService
 */
BackendSessionService.prototype.kickByUid = function(frontendId, uid, reason, cb) {
  let namespace = 'sys';
  let service = 'sessionRemote';
  let method = 'kickByUid';
  let args = [uid];
  if(typeof reason === 'function') {
    cb = reason;
  }else{
    args.push(reason);
  }
  rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
};

/**
 * Bind the session with the specified user id. It would finally invoke the
 * the sessionService.bind in the cooperating frontend server.
 *
 * @param  {String}   frontendId cooperating frontend server id
 * @param  {Number}   sid        session id
 * @param  {String}   uid        user id
 * @param  {Function} cb         callback function
 *
 * @memberOf BackendSessionService
 * @api private
 */
BackendSessionService.prototype.bind = function(frontendId, sid, uid, cb) {
  let namespace = 'sys';
  let service = 'sessionRemote';
  let method = 'bind';
  let args = [sid, uid];
  rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
};

/**
 * Unbind the session with the specified user id. It would finally invoke the
 * the sessionService.unbind in the cooperating frontend server.
 *
 * @param  {String}   frontendId cooperating frontend server id
 * @param  {Number}   sid        session id
 * @param  {String}   uid        user id
 * @param  {Function} cb         callback function
 *
 * @memberOf BackendSessionService
 * @api private
 */
BackendSessionService.prototype.unbind = function(frontendId, sid, uid, cb) {
  let namespace = 'sys';
  let service = 'sessionRemote';
  let method = 'unbind';
  let args = [sid, uid];
  rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
};

/**
 * Push the specified customized change to the frontend internal session.
 *
 * @param  {String}   frontendId cooperating frontend server id
 * @param  {Number}   sid        session id
 * @param  {String}   key        key in session that should be push
 * @param  {Object}   value      value in session, primitive js object
 * @param  {Function} cb         callback function
 *
 * @memberOf BackendSessionService
 * @api private
 */
BackendSessionService.prototype.push = function(frontendId, sid, key, value, cb) {
  let namespace = 'sys';
  let service = 'sessionRemote';
  let method = 'push';
  let args = [sid, key, value];
  rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
};

/**
 * Push all the customized changes to the frontend internal session.
 *
 * @param  {String}   frontendId cooperating frontend server id
 * @param  {Number}   sid        session id
 * @param  {Object}   settings   key/values in session that should be push
 * @param  {Function} cb         callback function
 *
 * @memberOf BackendSessionService
 * @api private
 */
BackendSessionService.prototype.pushAll = function(frontendId, sid, settings, cb) {
  let namespace = 'sys';
  let service = 'sessionRemote';
  let method = 'pushAll';
  let args = [sid, settings];
  rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
};

let rpcInvoke = function(app, sid, namespace, service, method, args, cb) {
  app.rpcInvoke(sid, {namespace: namespace, service: service, method: method, args: args}, cb);
};

/**
 * BackendSession is the proxy for the frontend internal session passed to handlers and
 * it helps to keep the key/value pairs for the server locally.
 * Internal session locates in frontend server and should not be accessed directly.
 *
 * The mainly operation on backend session should be read and any changes happen in backend
 * session is local and would be discarded in next request. You have to push the
 * changes to the frontend manually if necessary. Any push would overwrite the last push
 * of the same key silently and the changes would be saw in next request.
 * And you have to make sure the transaction outside if you would push the session
 * concurrently in different processes.
 *
 * See the api below for more details.
 *
 * @class
 * @constructor
 */
let BackendSession = function(opts, service) {
  for(let f in opts) {
    this[f] = opts[f];
  }
  this.__sessionService__ = service;
};

/**
 * Bind current session with the user id. It would push the uid to frontend
 * server and bind  uid to the frontend internal session.
 *
 * @param  {Number|String}   uid user id
 * @param  {Function} cb  callback function
 *
 * @memberOf BackendSession
 */
BackendSession.prototype.bind = function(uid, cb) {
  let self = this;
  this.__sessionService__.bind(this.frontendId, this.id, uid, function(err) {
    if(!err) {
      self.uid = uid;
    }
    utils.invokeCallback(cb, err);
  });
};

/**
 * Unbind current session with the user id. It would push the uid to frontend
 * server and unbind uid from the frontend internal session.
 *
 * @param  {Number|String}   uid user id
 * @param  {Function} cb  callback function
 *
 * @memberOf BackendSession
 */
BackendSession.prototype.unbind = function(uid, cb) {
  let self = this;
  this.__sessionService__.unbind(this.frontendId, this.id, uid, function(err) {
    if(!err) {
      self.uid = null;
    }
    utils.invokeCallback(cb, err);
  });
};

/**
 * Set the key/value into backend session.
 *
 * @param {String} key   key
 * @param {Object} value value
 */
BackendSession.prototype.set = function(key, value) {
  this.settings[key] = value;
};

/**
 * Get the value from backend session by key.
 *
 * @param  {String} key key
 * @return {Object}     value
 */
BackendSession.prototype.get = function(key) {
  return this.settings[key];
};

/**
 * Push the key/value in backend session to the front internal session.
 *
 * @param  {String}   key key
 * @param  {Function} cb  callback function
 */
BackendSession.prototype.push = function(key, cb) {
  this.__sessionService__.push(this.frontendId, this.id, key, this.get(key), cb);
};

/**
 * Push all the key/values in backend session to the frontend internal session.
 *
 * @param  {Function} cb callback function
 */
BackendSession.prototype.pushAll = function(cb) {
  this.__sessionService__.pushAll(this.frontendId, this.id, this.settings, cb);
};

/**
 * Export the key/values for serialization.
 *
 * @api private
 */
BackendSession.prototype.export = function() {
  let res = {};
  EXPORTED_FIELDS.forEach(function(field) {
    res[field] = this[field];
  });
  return res;
};

let BackendSessionCB = function(service, cb, err, sinfo) {
  if(err) {
    utils.invokeCallback(cb, err);
    return;
  }

  if(!sinfo) {
    utils.invokeCallback(cb);
    return;
  }
  let sessions = [];
  if(Array.isArray(sinfo)){
      // #getByUid
      for(let i = 0,k = sinfo.length;i<k;i++){
          sessions.push(service.create(sinfo[i]));
      }
  }
  else{
      // #get
      sessions = service.create(sinfo);
  }
  utils.invokeCallback(cb, null, sessions);
};