module Workarea
  module Releasable
    extend ActiveSupport::Concern
    include Mongoid::DocumentPath
    include Release::Activation
    include Segmentable

    included do
      field :active, type: Boolean, default: true, localize: Workarea.config.localized_active_fields
      attr_accessor :release_id

      has_many :changesets,
        class_name: 'Workarea::Release::Changeset',
        as: :releasable

      validate :slug_unchanged, on: :update

      define_model_callbacks :save_release_changes
      before_update :handle_release_changes
      after_find :load_release_changes
      after_destroy :destroy_embedded_changesets

      if Workarea.config.localized_active_fields
        I18n.for_each_locale { index("active.#{I18n.locale}" => 1) }
      else
        index(active: 1)
      end
    end

    def changesets_with_children
      criteria = Release::Changeset.any_of(
        { releasable_type: self.class.name, releasable_id: id }
      )

      embedded_children.each do |child|
        if child.respond_to?(:changesets_with_children)
          criteria.merge!(child.changesets_with_children)
        end
      end

      criteria
    end

    # A hash of changes for being set on the changeset. It's just a filtered
    # version of #changes from Mongoid.
    #
    # @return [Hash]
    #
    def release_changes
      ::Workarea::Release::Changes.new(self).to_h
    end

    def release_originals
      ::Workarea::Release::Changes.new(self).to_originals_h
    end

    # Get a new instance of this model loaded with changes for the release
    # passed in.
    #
    # @return [Releasable]
    #
    def in_release(release)
      if release.blank? && !changed? # No extra work necessary, return a copy
        result = dup
        result.id = id
        result.release_id = nil
        result
      elsif release.present? && !changed? # We don't have to reload from DB, just apply release changes to a copy
        result = dup
        result.id = id
        result.release_id = release.id
        release.preview.changesets_for(self).each { |cs| cs.apply_to(result) }
        result
      else
        Release.with_current(release) { self.class.find(id) }
      end
    end

    # Get a new instance of this model without any release changes. This a new
    # instance without any release changes applied.
    #
    # @return [Releasable]
    #
    def without_release
      in_release(nil)
    end

    # Skip the release changeset for the duration of the block. Used when
    # publishing a changeset, i.e. don't apply/save the release changes since
    # we actually want to publish.
    #
    # @return whatever the block returns
    #
    def skip_changeset
      @_skip_changeset = true
      yield

    ensure
      @_skip_changeset = false
    end

    # Persist a to be recalled for publishing later. This is where changesets
    # make it to the database.
    #
    # Will raise an error if the persistence goes wrong (it shouldn't)
    #
    # @param release_id [String]
    #
    def save_changeset(release)
      return unless release.present?

      changeset = release.changesets.find_or_initialize_by(releasable: self)

      run_callbacks :save_release_changes do
        if changeset.persisted? && release_changes.present?
          changeset.update!(changeset: release_changes, original: release_originals)
        elsif release_changes.present?
          changeset.document_path = document_path
          changeset.changeset = release_changes
          changeset.original = release_originals
          changeset.save!
        elsif changeset.persisted?
          changeset.destroy
        end
      end

      changes.each do |field, change|
        attributes[field] = change.first
      end
    end

    def destroy(*)
      if embedded? && Release.current.present?
        update!(active: false)
      else
        super
      end
    end

    private

    def load_release_changes
      return if readonly? || Release.current.blank? # Documents found with .only cause issues

      Release.current.preview.changesets_for(self).each { |c| c.apply_to(self) }
      self.release_id = Release.current.id
    end

    def handle_release_changes
      save_changeset(Release.current) unless @_skip_changeset
    end

    def destroy_embedded_changesets
      Release::Changeset.by_document_path(self).destroy_all
    end

    def slug_unchanged
      if Release.current.present? && changes['slug'].present?
        errors.add(:slug, 'cannot be changed for releases')
      end
    end
  end
end