text: define(function (require) {
    return function SavedObjectFactory(es, kbnIndex, Promise, Private, Notifier, indexPatterns) {
      var angular = require('angular');
      var errors = require('ui/errors');
      var _ = require('lodash');
      var slugifyId = require('ui/utils/slugify_id');
  
      var DocSource = Private(require('ui/courier/data_source/doc_source'));
      var SearchSource = Private(require('ui/courier/data_source/search_source'));
      var mappingSetup = Private(require('ui/utils/mapping_setup'));
  
      function SavedObject(config) {
        if (!_.isObject(config)) config = {};
  
        // save an easy reference to this
        var self = this;
  
        /************
         * Initialize config vars
         ************/
        // the doc which is used to store this object
        var docSource = new DocSource();
  
        // type name for this object, used as the ES-type
        var type = config.type;
  
        // Create a notifier for sending alerts
        var notify = new Notifier({
          location: 'Saved ' + type
        });
  
        // mapping definition for the fields that this object will expose
        var mapping = mappingSetup.expandShorthand(config.mapping);
  
        // default field values, assigned when the source is loaded
        var defaults = config.defaults || {};
  
        var afterESResp = config.afterESResp || _.noop;
        var customInit = config.init || _.noop;
  
        // optional search source which this object configures
        self.searchSource = config.searchSource && new SearchSource();
  
        // the id of the document
        self.id = config.id || void 0;
        self.defaults = config.defaults;
  
        /**
         * Asynchronously initialize this object - will only run
         * once even if called multiple times.
         *
         * @return {Promise}
         * @resolved {SavedObject}
         */
        self.init = _.once(function () {
          // ensure that the type is defined
          if (!type) throw new Error('You must define a type name to use SavedObject objects.');
  
          // tell the docSource where to find the doc
          docSource
          .index(kbnIndex)
          .type(type)
          .id(self.id);
  
          // check that the mapping for this type is defined
          return mappingSetup.isDefined(type)
          .then(function (defined) {
            // if it is already defined skip this step
            if (defined) return true;
  
            mapping.kibanaSavedObjectMeta = {
              properties: {
                // setup the searchSource mapping, even if it is not used but this type yet
                searchSourceJSON: {
                  type: 'string'
                }
              }
            };
  
            // tell mappingSetup to set type
            return mappingSetup.setup(type, mapping);
          })
          .then(function () {
            // If there is not id, then there is no document to fetch from elasticsearch
            if (!self.id) {
              // just assign the defaults and be done
              _.assign(self, defaults);
              return hydrateIndexPattern().then(function () {
                return afterESResp.call(self);
              });
            }
  
            // fetch the object from ES
            return docSource.fetch()
            .then(self.applyESResp);
          })
          .then(function () {
            return customInit.call(self);
          })
          .then(function () {
            // return our obj as the result of init()
            return self;
          });
        });
  
        self.applyESResp = function (resp) {
          self._source = _.cloneDeep(resp._source);
  
          if (resp.found != null && !resp.found) throw new errors.SavedObjectNotFound(type, self.id);
  
          var meta = resp._source.kibanaSavedObjectMeta || {};
          delete resp._source.kibanaSavedObjectMeta;
  
          if (!config.indexPattern && self._source.indexPattern) {
            config.indexPattern = self._source.indexPattern;
            delete self._source.indexPattern;
          }
  
          // assign the defaults to the response
          _.defaults(self._source, defaults);
  
          // transform the source using _deserializers
          _.forOwn(mapping, function ittr(fieldMapping, fieldName) {
            if (fieldMapping._deserialize) {
              self._source[fieldName] = fieldMapping._deserialize(self._source[fieldName], resp, fieldName, fieldMapping);
            }
          });
  
          // Give obj all of the values in _source.fields
          _.assign(self, self._source);
  
          return Promise.try(function () {
            parseSearchSource(meta.searchSourceJSON);
          })
          .then(hydrateIndexPattern)
          .then(function () {
            return Promise.cast(afterESResp.call(self, resp));
          })
          .then(function () {
            // Any time obj is updated, re-call applyESResp
            docSource.onUpdate().then(self.applyESResp, notify.fatal);
          });
        };
  
        function parseSearchSource(searchSourceJson) {
          if (!self.searchSource) return;
  
          // if we have a searchSource, set its state based on the searchSourceJSON field
          var state;
          try {
            state = JSON.parse(searchSourceJson);
          } catch (e) {
            state = {};
          }
  
          var oldState = self.searchSource.toJSON();
          var fnProps = _.transform(oldState, function (dynamic, val, name) {
            if (_.isFunction(val)) dynamic[name] = val;
          }, {});
  
          self.searchSource.set(_.defaults(state, fnProps));
        }
  
        /**
         * After creation or fetching from ES, ensure that the searchSources index indexPattern
         * is an bonafide IndexPattern object.
         *
         * @return {[type]} [description]
         */
        function hydrateIndexPattern() {
          return Promise.try(function () {
            if (self.searchSource) {
  
              var index = config.indexPattern || self.searchSource.getOwn('index');
              if (!index) return;
              if (config.clearSavedIndexPattern) {
                self.searchSource.set('index', undefined);
                return;
              }
  
              if (!(index instanceof indexPatterns.IndexPattern)) {
                index = indexPatterns.get(index);
              }
  
              return Promise.resolve(index).then(function (indexPattern) {
                self.searchSource.set('index', indexPattern);
              });
            }
          });
        }
  
        /**
         * Serialize this object
         *
         * @return {Object}
         */
        self.serialize = function () {
          var body = {};
  
          _.forOwn(mapping, function (fieldMapping, fieldName) {
            if (self[fieldName] != null) {
              body[fieldName] = (fieldMapping._serialize)
                ? fieldMapping._serialize(self[fieldName])
                : self[fieldName];
            }
          });
  
          if (self.searchSource) {
            body.kibanaSavedObjectMeta = {
              searchSourceJSON: angular.toJson(_.omit(self.searchSource.toJSON(), ['sort', 'size']))
            };
          }
  
          return body;
        };
  
        /**
         * Save this object
         *
         * @return {Promise}
         * @resolved {String} - The id of the doc
         */
        self.save = function () {
  
          var body = self.serialize();
  
          // Slugify the object id
          self.id = slugifyId(self.id);
  
          // ensure that the docSource has the current self.id
          docSource.id(self.id);
  
          // index the document
          return self.saveSource(body);
        };
  
        self.saveSource = function (source) {
          var finish = function (id) {
            self.id = id;
            return es.indices.refresh({
              index: kbnIndex
            })
            .then(function () {
              return self.id;
            });
          };
  
          return docSource.doCreate(source)
          .then(finish)
          .catch(function (err) {
            // record exists, confirm overwriting
            if (_.get(err, 'origError.status') === 409) {
              var confirmMessage = 'Are you sure you want to overwrite ' + self.title + '?';
  
              if (window.confirm(confirmMessage)) { // eslint-disable-line no-alert
                return docSource.doIndex(source).then(finish);
              }
  
              // if the user doesn't overwrite record, just swallow the error
              return;
            }
            return Promise.reject(err);
          });
        };
  
        /**
         * Destroy this object
         *
         * @return {undefined}
         */
        self.destroy = function () {
          docSource.cancelQueued();
          if (self.searchSource) {
            self.searchSource.cancelQueued();
          }
        };
  
        /**
         * Delete this object from Elasticsearch
         * @return {promise}
         */
        self.delete = function () {
          return es.delete({
            index: kbnIndex,
            type: type,
            id: this.id
          }).then(function () {
            return es.indices.refresh({
              index: kbnIndex
            });
          });
        };
      }
  
      return SavedObject;
    };
  });
  