/*!
 * Dependencies.
 */

var assert = require('assert')
  , loopback = require('../loopback')
  , debug = require('debug')
  , path = require('path')
  , request = require('browser-request')
  , Connector = require('loopback-datasource-juggler').Connector
  , util = require('util');

/*!
 * Export the ServerConnector class.
 */

module.exports = ServerConnector;

/*!
 * Create an instance of the connector with the given `settings`.
 */

function ServerConnector(settings, dataSource) {
  Connector.call(this, 'server', settings);
  this.settings = settings;
  this.dataSource = dataSource;
  dataSource.DataAccessObject = dataSource.constructor.DataAccessObject;
  settings.base = settings.base || '/';
  dataSource.connect = this.connect;
}
util.inherits(ServerConnector, Connector);

ServerConnector.initialize = function(dataSource, callback) {
  var connector = dataSource.connector = new ServerConnector(dataSource.settings, dataSource);

  var remoteModels = connector.settings.discover;
  if(remoteModels) {
    remoteModels = remoteModels.sort(function(remoteModel) {
      var settings = remoteModel.settings;
      var trackChanges = settings && settings.trackChanges;
      return trackChanges ? 1 : 0;
    });
    remoteModels.forEach(connector.buildModel.bind(connector));
  }
  callback();
}

ServerConnector.prototype.connect = function(callback) {
  process.nextTick(function () {
    callback && callback(null, self.db);
  });
}

ServerConnector.prototype.requestModel = function(model, req, callback) {
  var Model = loopback.getModel(model);
  var modelPath = '/' + Model.pluralModelName;
  var url = path.join(this.settings.base, modelPath, req.url || '');
  this.request(url, req, callback);
}

ServerConnector.prototype.requestModelById = function(model, id, req, callback) {
  var Model = loopback.getModel(model);
  var modelPath = '/' + Model.pluralModelName;
  var url = path.join(this.settings.base, modelPath, id.toString(), req.url || '');
  this.request(url, req, callback);
}

ServerConnector.prototype.request = function(url, req, callback) {
  request({
    url: url,
    method: req.method || 'GET',
    body: req.body,
    json: req.json || true
  }, function(err, res, body) {
    if(res.statusCode >= 400) {
      if(res.statusCode === 404 && req.ignoreNotFound) {
        return callback && callback(null, null);
      }
      err = body.error || body;
      body = undefined;
    }
    callback && callback(err, body);
  });
}

ServerConnector.prototype.buildModel = function(remoteModel) {
  var modelName = remoteModel.modelName;
  var dataSource = this.dataSource;
  var connector = this;

  if(remoteModel.settings && remoteModel.settings.trackChanges) {
    remoteModel.settings.trackChanges = false;
  }

  var Model = loopback.createModel(
    modelName,
    remoteModel.properties || {},
    remoteModel.settings
  );

  Model.attachTo(dataSource);

  return;

  if(!Model.defineMethod) {
    Model.defineMethod = function defineMethod(method) {
      var isStatic = method.fullName.indexOf('.prototype.') === -1;
      var scope = isStatic ? Model : Model.prototype;
      var methodName = isStatic ? method.name : method.name.replace('prototype.', '');

      if(methodName === 'Change') {
        return; // skip
      }

      scope[methodName] = function() {
        console.log(method.name);
        var callback = arguments[arguments.length - 1];
        var ctx = new Context(
          connector.settings.base,
          remoteModel,
          Model,
          method,
          arguments,
          callback
        );
        ctx.invoke();
      };
    }
  }

  remoteModel.methods.forEach(Model.defineMethod.bind(Model));
}

/**
 * Create a new model instance for the given data
 * @param {String} model The model name
 * @param {Object} data The model data
 * @param {Function} [callback] The callback function
 */

ServerConnector.prototype.create = function (model, data, callback) {
  this.requestModel(model, {
    method: 'POST',
    body: data
  }, callback);
};

/**
 * Save the model instance for the given data
 * @param {String} model The model name
 * @param {Object} data The model data
 * @param {Function} [callback] The callback function
 */

ServerConnector.prototype.save = function (model, data, callback) {
  var idValue = this.getIdValue(model, data);
  if(idValue) {
    this.requestModel(model, {
      method: 'PUT',
      body: data
    }, callback);
  } else {
    this.create(model, data, callback);
  }
};

/**
 * Check if a model instance exists by id
 * @param {String} model The model name
 * @param {*} id The id value
 * @param {Function} [callback] The callback function
 */

ServerConnector.prototype.exists = function (model, id, callback) {
  this.requestModel(model, {
    url: '/exists'
  }, callback);
};

/**
 * Find a model instance by id
 * @param {String} model The model name
 * @param {*} id The id value
 * @param {Function} [callback] The callback function
 */

ServerConnector.prototype.find = function find(model, id, callback) {
  this.requestModelById(model, id, {
    ignoreNotFound: true
  }, callback);
};

/**
 * Update if the model instance exists with the same id or create a new instance
 *
 * @param {String} model The model name
 * @param {Object} data The model instance data
 * @param {Function} [callback] The callback function
 */

ServerConnector.prototype.updateOrCreate = function updateOrCreate(model, data, callback) {
  var self = this;
  var idValue = self.getIdValue(model, data);

  if (idValue === null || idValue === undefined) {
    return this.create(data, callback);
  }
  this.find(model, idValue, function (err, inst) {
    if (err) {
      return callback(err);
    }
    if (inst) {
      self.updateAttributes(model, idValue, data, callback);
    } else {
      self.create(model, data, function (err, id) {
        if (err) {
          return callback(err);
        }
        if (id) {
          self.setIdValue(model, data, id);
          callback(null, data);
        } else {
          callback(null, null); // wtf?
        }
      });
    }
  });
};

/**
 * Delete a model instance by id
 * @param {String} model The model name
 * @param {*} id The id value
 * @param [callback] The callback function
 */

ServerConnector.prototype.destroy = function destroy(model, id, callback) {
  this.requestModelById(model, id, {
    method: 'DELETE',
    json: false
  }, callback);
};

/**
 * Find matching model instances by the filter
 *
 * @param {String} model The model name
 * @param {Object} filter The filter
 * @param {Function} [callback] The callback function
 */

ServerConnector.prototype.all = function all(model, filter, callback) {
  this.requestModel(model, {
    query: {filter: filter}
  }, callback);
};

/**
 * Delete all instances for the given model
 * @param {String} model The model name
 * @param {Object} [where] The filter for where
 * @param {Function} [callback] The callback function
 */

ServerConnector.prototype.destroyAll = function destroyAll(model, where, callback) {
  this.requestModel(model, {
    method: 'DELETE',
    query: {where: where}
  }, callback);
};

/**
 * Count the number of instances for the given model
 *
 * @param {String} model The model name
 * @param {Function} [callback] The callback function
 * @param {Object} filter The filter for where
 *
 */

ServerConnector.prototype.count = function count(model, callback, where) {
  this.requestModel(model, {
    url: '/count',
    query: {where: where}
  }, callback);
};

/**
 * Update properties for the model instance data
 * @param {String} model The model name
 * @param {Object} data The model data
 * @param {Function} [callback] The callback function
 */

ServerConnector.prototype.updateAttributes = function updateAttrs(model, id, data, callback) {
  this.requestModelById(model, id, {
    method: 'PUT',
    url: '/updateAttributes'
  }, callback);
};

function Context(base, meta, model, method, args, callback) {
  this.base = base;
  this.meta = meta;
  this.model = model;
  this.method = method;
  this.args = this.mapArgs(args);
  this.callback = callback;
}

/**
 * Build an http request object from the `context`.
 * @return {Object} request
 */

Context.prototype.toRequest = function() {
  return {
    url: this.url(),
    query: this.query(),
    method: this.verb(),
    body: this.body(),
    headers: this.headers(),
    json: this.isJSON()
  }
}

Context.prototype.isJSON = function() {
  return true;
}

Context.prototype.url = function() {
  var ctx = this;
  var args = this.args;
  var url = path.join(
    this.base,
    this.meta.baseRoute.path,
    this.route().path
  );

  // replace url fragments with url params
  this.method.accepts.forEach(function(param) {
    var argName = param.arg;
    var val = args[argName];
    if(param && param.http && param.http.source === 'path') {
      url = url.replace(':' + argName, val);
    }
  });
  return url;
}

Context.prototype.query = function() {
  var accepts = this.method.accepts;
  var queryParams;
  var ctx = this;

  if(accepts && accepts.length) {
    accepts.forEach(function(param) {
      var http = param.http || {};
      var explicit = http.source === 'query';
      var implicit = http.source !== 'body' && http.source !== 'url';
      
      if(explicit || implicit) {
        queryParams = queryParams || {};
        queryParams[param.arg] = ctx.args[param.arg];
      }
    });
  }

  return queryParams;
}

Context.prototype.route = function() {
  var routes = this.method.routes;

  return routes[0] || {path: '/', verb: 'GET'};
}

Context.prototype.verb = function() {
  return this.route().verb.toUpperCase();
}

Context.prototype.body = function() {
  var accepts = this.method.accepts;
  var body;
  var ctx = this;

  if(accepts && accepts.length) {
    accepts.forEach(function(param) {
      var http = param.http || {};
      var explicit = http.source === 'body';
      
      if(explicit) {
        body = ctx.args[param.arg];
      }
    });
  }

  return body;
}

Context.prototype.headers = function() {
  return {};
}

Context.prototype.mapArgs = function(args) {
  var accepts = this.method.accepts || [];
  var args = Array.prototype.slice.call(args);
  var result = {};
  var supportedSources = ['body', 'form', 'query', 'path'];

  accepts.forEach(function(param) {
    if(param.http && param.http.source) {
      // skip explicit unknown sources
      if(supportedSources.indexOf(param.http.source) === -1) return;
    }

    var val = args.shift();
    var type = typeof val;
    if(Array.isArray(val)) {
      type = 'array';
    }

    // skip all functions
    if(type === 'function') return;

    switch(param.type) {
      case 'any':
      case type:
        result[param.arg] = val;
      break;
      default:
        // skip this param
        args.unshift(val);
      break;
    }
  });

  return result;
}

Context.prototype.handleResponse = function(err, res, body) {
  // TODO handle `returns` correctly
  this.callback.call(this, err, body);
}

Context.prototype.invoke = function() {
  var req = this.toRequest();
  request(req, this.handleResponse.bind(this));
}