'use strict';

var validator = require('./utils/validator-extras').validator
  , extendModelValidations = require('./utils/validator-extras').extendModelValidations
  , Utils = require('./utils')
  , sequelizeError = require('./errors')
  , Promise = require('./promise')
  , DataTypes = require('./data-types')
  , _ = require('lodash');

/**
 * The Main Instance Validator.
 *
 * @param {Instance} modelInstance The model instance.
 * @param {Object} options A dict with options.
 * @constructor
 */
var InstanceValidator = module.exports = function(modelInstance, options) {
  options = _.clone(options) || {};

  if (options.fields && !options.skip) {
    options.skip = Utils._.difference(Object.keys(modelInstance.Model.attributes), options.fields);
  }

  // assign defined and default options
  this.options = Utils._.defaults(options, {
    skip: []
  });

  this.modelInstance = modelInstance;

  /**
   * Exposes a reference to validator.js. This allows you to add custom validations using `validator.extend`
   * @name validator
   */
  this.validator = validator;

  /**
   *  All errors will be stored here from the validations.
   *
   * @type {Array} Will contain keys that correspond to attributes which will
   *   be Arrays of Errors.
   */
  this.errors = [];

  /** @type {boolean} Indicates if validations are in progress */
  this.inProgress = false;

  extendModelValidations(modelInstance);
};

/** @define {string} The error key for arguments as passed by custom validators */
InstanceValidator.RAW_KEY_NAME = '__raw';

/**
 * The main entry point for the Validation module, invoke to start the dance.
 *
 * @return {Promise}
 */
InstanceValidator.prototype.validate = function() {
  if (this.inProgress) {
    throw new Error('Validations already in progress.');
  }
  this.inProgress = true;

  var self = this;
  return Promise.all([
    self._builtinValidators(),
    self._customValidators()
  ].map(function(promise) {
    return promise.reflect();
  })).then(function() {
    if (self.errors.length) {
      return new sequelizeError.ValidationError(null, self.errors);
    } else {
      return null;
    }
  });
};

/**
 * Invoke the Validation sequence:
 *   - Before Validation Model Hooks
 *   - Validation
 *   - On validation success: After Validation Model Hooks
 *   - On validation failure: Validation Failed Model Hooks
 *
 * @return {Promise}
 */
InstanceValidator.prototype.hookValidate = function() {
  var self = this;
  return self.modelInstance.Model.runHooks('beforeValidate', self.modelInstance, self.options).then(function() {
    return self.validate().then(function(error) {
      if (error) {
        return self.modelInstance.Model.runHooks('validationFailed', self.modelInstance, self.options, error).then(function(newError) {
          throw newError || error;
        });
      }
    });
  }).then(function() {
    return self.modelInstance.Model.runHooks('afterValidate', self.modelInstance, self.options);
  }).return(self.modelInstance);
};

/**
 * Will run all the built-in validators.
 *
 * @return {Promise(Array.<Promise.PromiseInspection>)} A promise from .reflect().
 * @private
 */
InstanceValidator.prototype._builtinValidators = function() {
  var self = this;

  // promisify all attribute invocations
  var validators = [];
  Utils._.forIn(this.modelInstance.rawAttributes, function(rawAttribute, field) {
    if (self.options.skip.indexOf(field) >= 0) {
      return;
    }

    var value = self.modelInstance.dataValues[field];

    if (!rawAttribute._autoGenerated && !rawAttribute.autoIncrement) {
      // perform validations based on schema
      self._validateSchema(rawAttribute, field, value);
    }

    if (self.modelInstance.validators.hasOwnProperty(field)) {
      validators.push(self._builtinAttrValidate.call(self, value, field).reflect());
    }
  });

  return Promise.all(validators);
};

/**
 * Will run all the custom validators.
 *
 * @return {Promise(Array.<Promise.PromiseInspection>)} A promise from .reflect().
 * @private
 */
InstanceValidator.prototype._customValidators = function() {
  var validators = [];
  var self = this;
  Utils._.each(this.modelInstance.$modelOptions.validate, function(validator, validatorType) {
    if (self.options.skip.indexOf(validatorType) >= 0) {
      return;
    }

    var valprom = self._invokeCustomValidator(validator, validatorType)
      // errors are handled in settling, stub this
      .catch(function() {})
      .reflect();

    validators.push(valprom);
  });

  return Promise.all(validators);
};

/**
 * Validate a single attribute with all the defined built-in validators.
 *
 * @param {*} value Anything.
 * @param {string} field The field name.
 * @return {Promise} A promise, will always resolve,
 *   auto populates error on this.error local object.
 * @private
 */
InstanceValidator.prototype._builtinAttrValidate = function(value, field) {
  var self = this;
  // check if value is null (if null not allowed the Schema pass will capture it)
  if (value === null || typeof value === 'undefined') {
    return Promise.resolve();
  }

  // Promisify each validator
  var validators = [];
  Utils._.forIn(this.modelInstance.validators[field], function(test,
    validatorType) {

    if (['isUrl', 'isURL', 'isEmail'].indexOf(validatorType) !== -1) {
      // Preserve backwards compat. Validator.js now expects the second param to isURL and isEmail to be an object
      if (typeof test === 'object' && test !== null && test.msg) {
        test = {
          msg: test.msg
        };
      } else if (test === true) {
        test = {};
      }
    }

    // Check for custom validator.
    if (typeof test === 'function') {
      return validators.push(self._invokeCustomValidator(test, validatorType, true, value, field).reflect());
    }

    var validatorPromise = self._invokeBuiltinValidator(value, test, validatorType, field);
    // errors are handled in settling, stub this
    validatorPromise.catch(function() {});
    validators.push(validatorPromise.reflect());
  });

  return Promise.all(validators).then(this._handleReflectedResult.bind(this, field));
};

/**
 * Prepare and invoke a custom validator.
 *
 * @param {Function} validator The custom validator.
 * @param {string} validatorType the custom validator type (name).
 * @param {boolean=} optAttrDefined Set to true if custom validator was defined
 *   from the Attribute
 * @return {Promise} A promise.
 * @private
 */
InstanceValidator.prototype._invokeCustomValidator = Promise.method(function(validator, validatorType, optAttrDefined, optValue, optField) {
  var validatorFunction = null;  // the validation function to call
  var isAsync = false;

  var validatorArity = validator.length;
  // check if validator is async and requires a callback
  var asyncArity = 1;
  var errorKey = validatorType;
  var invokeArgs;
  if (optAttrDefined) {
    asyncArity = 2;
    invokeArgs = optValue;
    errorKey = optField;
  }
  if (validatorArity === asyncArity) {
    isAsync = true;
  }

  if (isAsync) {
    if (optAttrDefined) {
      validatorFunction = Promise.promisify(validator.bind(this.modelInstance, invokeArgs));
    } else {
      validatorFunction = Promise.promisify(validator.bind(this.modelInstance));
    }
    return validatorFunction().catch(this._pushError.bind(this, false, errorKey));
  } else {
    return Promise.try(validator.bind(this.modelInstance, invokeArgs)).catch(this._pushError.bind(this, false, errorKey));
  }
});

/**
 * Prepare and invoke a build-in validator.
 *
 * @param {*} value Anything.
 * @param {*} test The test case.
 * @param {string} validatorType One of known to Sequelize validators.
 * @param {string} field The field that is being validated
 * @return {Object} An object with specific keys to invoke the validator.
 * @private
 */
InstanceValidator.prototype._invokeBuiltinValidator = Promise.method(function(value, test, validatorType, field) {
  var self = this;
  // Cast value as string to pass new Validator.js string requirement
  var valueString = String(value);
  // check if Validator knows that kind of validation test
  if (typeof validator[validatorType] !== 'function') {
    throw new Error('Invalid validator function: ' + validatorType);
  }
  var validatorArgs = self._extractValidatorArgs(test, validatorType, field);
  if (!validator[validatorType].apply(validator, [valueString].concat(validatorArgs))) {
  // extract the error msg
    throw new Error(test.msg || 'Validation ' + validatorType + ' failed');
  }
});

/**
 * Will extract arguments for the validator.
 *
 * @param {*} test The test case.
 * @param {string} validatorType One of known to Sequelize validators.
 * @param {string} field The field that is being validated.
 * @private
 */
InstanceValidator.prototype._extractValidatorArgs = function(test, validatorType, field) {
  var validatorArgs = test.args || test;
  var isLocalizedValidator = typeof(validatorArgs) !== 'string' && (validatorType === 'isAlpha' || validatorType === 'isAlphanumeric' || validatorType === 'isMobilePhone');

  if (!Array.isArray(validatorArgs)) {
    if (validatorType === 'isImmutable') {
      validatorArgs = [validatorArgs, field];
    } else if (isLocalizedValidator || validatorType === 'isIP') {
      validatorArgs = [];
    } else {
      validatorArgs = [validatorArgs];
    }
  } else {
    validatorArgs = validatorArgs.slice(0);
  }
  return validatorArgs;
};

/**
 * Will validate a single field against its schema definition (isnull).
 *
 * @param {Object} rawAttribute As defined in the Schema.
 * @param {string} field The field name.
 * @param {*} value anything.
 * @private
 */
InstanceValidator.prototype._validateSchema = function(rawAttribute, field, value) {
  var error;

  if (rawAttribute.allowNull === false && ((value === null) || (value === undefined))) {
    error = new sequelizeError.ValidationErrorItem(field + ' cannot be null', 'notNull Violation', field, value);
    this.errors.push(error);
  }

  if (rawAttribute.type === DataTypes.STRING || rawAttribute.type instanceof DataTypes.STRING || rawAttribute.type === DataTypes.TEXT || rawAttribute.type instanceof DataTypes.TEXT) {
    if (Array.isArray(value) || (_.isObject(value) && !value._isSequelizeMethod) && !Buffer.isBuffer(value)) {
      error = new sequelizeError.ValidationErrorItem(field + ' cannot be an array or an object', 'string violation', field, value);
      this.errors.push(error);
    }
  }
};


/**
 * Handles the returned result of a Promise.reflect.
 *
 * If errors are found it populates this.error.
 *
 * @param {string} field The attribute name.
 * @param {Array.<Promise.PromiseInspection>} Promise inspection objects.
 * @private
 */
InstanceValidator.prototype._handleReflectedResult = function(field, promiseInspections) {
  var self = this;
  promiseInspections.forEach(function(promiseInspection) {
    if (promiseInspection.isRejected()) {
      var rejection = promiseInspection.error();
      self._pushError(true, field, rejection);
    }
  });
};

/**
 * Signs all errors retaining the original.
 *
 * @param {boolean} isBuiltin Determines if error is from builtin validator.
 * @param {string} errorKey The error key to assign on this.errors object.
 * @param {Error|string} rawError The original error.
 * @private
 */
InstanceValidator.prototype._pushError = function(isBuiltin, errorKey, rawError) {
  var message = rawError.message || rawError || 'Validation error';
  var error = new sequelizeError.ValidationErrorItem(message, 'Validation error', errorKey, rawError);
  error[InstanceValidator.RAW_KEY_NAME] = rawError;

  this.errors.push(error);
};
