'use strict';

const applyTimestampsToChildren = require('../update/applyTimestampsToChildren');
const applyTimestampsToUpdate = require('../update/applyTimestampsToUpdate');
const cast = require('../../cast');
const castUpdate = require('../query/castUpdate');
const setDefaultsOnInsert = require('../setDefaultsOnInsert');

/*!
 * Given a model and a bulkWrite op, return a thunk that handles casting and
 * validating the individual op.
 */

module.exports = function castBulkWrite(model, op, options) {
  const now = model.base.now();
  const schema = model.schema;

  if (op['insertOne']) {
    return (callback) => {
      const doc = new model(op['insertOne']['document']);
      if (model.schema.options.timestamps != null) {
        doc.initializeTimestamps();
      }
      if (options.session != null) {
        doc.$session(options.session);
      }
      op['insertOne']['document'] = doc;
      op['insertOne']['document'].validate({ __noPromise: true }, function(error) {
        if (error) {
          return callback(error, null);
        }
        callback(null);
      });
    };
  } else if (op['updateOne']) {
    return (callback) => {
      try {
        if (!op['updateOne']['filter']) throw new Error('Must provide a filter object.');
        if (!op['updateOne']['update']) throw new Error('Must provide an update object.');

        _addDiscriminatorToObject(schema, op['updateOne']['filter']);
        op['updateOne']['filter'] = cast(model.schema, op['updateOne']['filter']);
        op['updateOne']['update'] = castUpdate(model.schema, op['updateOne']['update'], {
          strict: model.schema.options.strict,
          overwrite: false
        });
        if (op['updateOne'].setDefaultsOnInsert) {
          setDefaultsOnInsert(op['updateOne']['filter'], model.schema, op['updateOne']['update'], {
            setDefaultsOnInsert: true,
            upsert: op['updateOne'].upsert
          });
        }
        if (model.schema.$timestamps != null) {
          const createdAt = model.schema.$timestamps.createdAt;
          const updatedAt = model.schema.$timestamps.updatedAt;
          applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateOne']['update'], {});
        }
        applyTimestampsToChildren(now, op['updateOne']['update'], model.schema);
      } catch (error) {
        return callback(error, null);
      }

      callback(null);
    };
  } else if (op['updateMany']) {
    return (callback) => {
      try {
        if (!op['updateMany']['filter']) throw new Error('Must provide a filter object.');
        if (!op['updateMany']['update']) throw new Error('Must provide an update object.');

        _addDiscriminatorToObject(schema, op['updateMany']['filter']);
        op['updateMany']['filter'] = cast(model.schema, op['updateMany']['filter']);
        op['updateMany']['update'] = castUpdate(model.schema, op['updateMany']['update'], {
          strict: model.schema.options.strict,
          overwrite: false
        });
        if (op['updateMany'].setDefaultsOnInsert) {
          setDefaultsOnInsert(op['updateMany']['filter'], model.schema, op['updateMany']['update'], {
            setDefaultsOnInsert: true,
            upsert: op['updateMany'].upsert
          });
        }
        if (model.schema.$timestamps != null) {
          const createdAt = model.schema.$timestamps.createdAt;
          const updatedAt = model.schema.$timestamps.updatedAt;
          applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateMany']['update'], {});
        }
        applyTimestampsToChildren(now, op['updateMany']['update'], model.schema);
      } catch (error) {
        return callback(error, null);
      }

      callback(null);
    };
  } else if (op['replaceOne']) {
    return (callback) => {
      _addDiscriminatorToObject(schema, op['replaceOne']['filter']);
      try {
        op['replaceOne']['filter'] = cast(model.schema,
          op['replaceOne']['filter']);
      } catch (error) {
        return callback(error, null);
      }

      // set `skipId`, otherwise we get "_id field cannot be changed"
      const doc = new model(op['replaceOne']['replacement'], null, true);
      if (model.schema.options.timestamps != null) {
        doc.initializeTimestamps();
      }
      if (options.session != null) {
        doc.$session(options.session);
      }
      op['replaceOne']['replacement'] = doc;

      op['replaceOne']['replacement'].validate({ __noPromise: true }, function(error) {
        if (error) {
          return callback(error, null);
        }
        callback(null);
      });
    };
  } else if (op['deleteOne']) {
    return (callback) => {
      _addDiscriminatorToObject(schema, op['deleteOne']['filter']);
      try {
        op['deleteOne']['filter'] = cast(model.schema,
          op['deleteOne']['filter']);
      } catch (error) {
        return callback(error, null);
      }

      callback(null);
    };
  } else if (op['deleteMany']) {
    return (callback) => {
      _addDiscriminatorToObject(schema, op['deleteMany']['filter']);
      try {
        op['deleteMany']['filter'] = cast(model.schema,
          op['deleteMany']['filter']);
      } catch (error) {
        return callback(error, null);
      }

      callback(null);
    };
  } else {
    return (callback) => {
      callback(new Error('Invalid op passed to `bulkWrite()`'), null);
    };
  }
};

function _addDiscriminatorToObject(schema, obj) {
  if (schema == null) {
    return;
  }
  if (schema.discriminatorMapping && !schema.discriminatorMapping.isRoot) {
    obj[schema.discriminatorMapping.key] = schema.discriminatorMapping.value;
  }
}