module Workarea
  class Release
    class Changeset
      include ApplicationDocument

      field :changeset, type: Hash, default: {}
      field :undo, type: Hash, default: {}

      embeds_many :document_path, class_name: 'Mongoid::DocumentPath::Node'

      belongs_to :release, class_name: 'Workarea::Release', index: true
      belongs_to :releasable, polymorphic: true, index: true, optional: true

      index(
        { 'document_path.type' => 1, 'document_path.document_id' => 1 },
        { name: 'document_path' }
      )

      # Whether these value changes to this field should be included when
      # saving a changeset. Used in building changeset hashes.
      #
      # @param attribute [String]
      # @param old_value [Object]
      # @param new_value [Object]
      #
      # @return [Boolean]
      #
      def self.track_change?(attribute, old_value, new_value)
        !attribute.in?(Workarea.config.untracked_release_changes_fields) &&
          (old_value.present? || new_value.present?)
      end

      # Finds changeset by whether the passed document is in the document
      # path of the changeset. Useful for showing embedded changes in the
      # admin, e.g. showing content block changes as part of the timeline
      # for the content object.
      #
      # @param document [Mongoid::Document]
      # @return [Mongoid::Criteria]
      #
      def self.by_document_path(document)
        where(
          'document_path.type' => document.class.name,
          'document_path.document_id' => document.id.to_s
        )
      end

      # Find changesets by whether the passed class is in the document path of
      # the changeset. Used in the admin for display embedded changesets by
      # parent type, e.g. filtering activity by content and seeing content
      # block changes.
      #
      # @param klass [Class]
      # @return [Mongoid::Criteria]
      #
      def self.by_document_path_type(klass)
        where('document_path.type' => klass.name)
      end

      def changed_fields
        changeset.keys
      end

      # Apply (but do not save) the changes represented by this changeset to
      # the model passed in.
      #
      # @param model [Mongoid::Document]
      #
      def apply_to(model)
        apply_changeset(model, changeset)
      end

      # Make the changes represented by this changeset live. Used when
      # publishing a release.
      #
      # Builds and saves the undo in case that's desired later.
      #
      # @return [Boolean]
      #
      def publish!
        return false if releasable_from_document_path.blank?

        build_undo
        apply_to(releasable_from_document_path)

        releasable_from_document_path.skip_changeset do
          releasable_from_document_path.save!
        end

        save! # saves undo
      end

      # Apply the changes in the undo hash on this changeset and save to make
      # them live. Used when undoing a release.
      #
      # @return [Boolean]
      #
      def undo!
        apply_changeset(releasable_from_document_path, undo)

        releasable_from_document_path.skip_changeset do
          releasable_from_document_path.save!
        end
      end

      def releasable_from_document_path
        return @releasable_from_document_path if defined?(@releasable_from_document_path)

        @releasable_from_document_path =
          begin
            Mongoid::DocumentPath.find(document_path)
          rescue Mongoid::Errors::DocumentNotFound
            nil
          end
      end

      private

      def apply_changeset(model, changeset)
        changeset.each do |field, new_value|
          model.send(:attribute_will_change!, field) # required for correct dirty tracking
          model.attributes[field] = new_value
        end
      end

      def build_undo(model = releasable_from_document_path)
        self.undo = changeset.keys.inject({}) do |memo, key|
          old_value = model.attributes[key]
          new_value = changeset[key]

          if self.class.track_change?(key, old_value, new_value)
            memo[key] = old_value
          end

          memo
        end
      end
    end
  end
end