class CollectionItem < ApplicationRecord
  APPROVAL_OPTIONS = [
    ["", :unreviewed],
    [ts("Approved"), :approved],
    [ts("Rejected"), :rejected]
  ]

  belongs_to :collection, inverse_of: :collection_items
  belongs_to :item, polymorphic: :true, inverse_of: :collection_items, touch: true
  belongs_to :work,  class_name: "Work", foreign_key: "item_id", inverse_of: :collection_items
  belongs_to :bookmark, class_name: "Bookmark", foreign_key: "item_id"

  validates_uniqueness_of :collection_id, scope: [:item_id, :item_type],
    message: ts("already contains this item.")

  enum :user_approval_status, {
    rejected: -1,
    unreviewed: 0,
    approved: 1
  }, suffix: :by_user

  enum :collection_approval_status, {
    rejected: -1,
    unreviewed: 0,
    approved: 1
  }, suffix: :by_collection

  validate :collection_is_open, on: :create
  def collection_is_open
    if self.new_record? && self.collection && self.collection.closed? && !self.collection.user_is_maintainer?(User.current_user)
      errors.add(:base, ts("The collection %{title} is not currently open.", title: self.collection.title))
    end
  end

  scope :include_for_works, -> { includes(item: :pseuds) }
  scope :unrevealed, -> { where(unrevealed: true) }
  scope :anonymous, -> { where(anonymous:  true) }

  def self.for_user(user=User.current_user)
    # get ids of user's bookmarks and works
    bookmark_ids = Bookmark.joins(:pseud).where("pseuds.user_id = ?", user.id).pluck(:id)
    work_ids = Work.joins(:pseuds).where("pseuds.user_id = ?", user.id).pluck(:id)
    # now return the relation
    where("(item_id IN (?) AND item_type = 'Work') OR (item_id IN (?) AND item_type = 'Bookmark')", work_ids, bookmark_ids)
  end

  scope :invited_by_collection, -> { approved_by_collection.unreviewed_by_user }
  scope :approved_by_both, -> { approved_by_collection.approved_by_user }

  before_save :set_anonymous_and_unrevealed
  def set_anonymous_and_unrevealed
    if self.new_record? && collection
      self.unrevealed = true if collection.reload.unrevealed?
      self.anonymous = true if collection.reload.anonymous?
    end
  end

  after_save :update_work
  after_destroy :update_work

  # Set associated works to anonymous or unrevealed as appropriate.
  #
  # Inverses are set up properly on self.item, so we use that field to check
  # whether we're currently in the process of saving a brand new work, or
  # whether the work is in the process of being destroyed. (In which case we
  # rely on the Work's callbacks to set anon/unrevealed status properly.) But
  # because we want to discard changes made in preview mode, we perform the
  # actual anon/unrevealed updates on self.work, which doesn't have proper
  # inverses and therefore is freshly loaded from the database.
  def update_work
    return unless item.is_a?(Work) && item.persisted? && !item.saved_change_to_id?

    if work.present?
      work.update_anon_unrevealed

      # For a more helpful error message, raise an error saying that the work
      # is invalid if we fail to save it.
      raise ActiveRecord::RecordInvalid, work unless work.save(validate: false)
    end
  end

  # Poke the item if it's just been approved or unapproved so it gets picked up by the search index
  after_update :update_item_for_status_change
  def update_item_for_status_change
    if saved_change_to_user_approval_status? || saved_change_to_collection_approval_status?
      item.save!(validate: false)
    end
  end

  after_create_commit :notify_of_association
  def notify_of_association
    email_notify = self.collection.collection_preference &&
                    self.collection.collection_preference.email_notify

    if email_notify && !self.collection.email.blank?
      CollectionMailer.item_added_notification(item_id, collection_id, item_type).deliver_later
    end
  end

  after_create_commit :notify_archivist_added
  # Sends emails to item creator(s) in the case that an archivist
  # has added them to the collection.
  def notify_archivist_added
    return unless item.is_a?(Work) && User.current_user&.archivist && collection.user_is_maintainer?(User.current_user)

    item.users.each do |email_recipient|
      next if email_recipient.preference.collection_emails_off

      I18n.with_locale(email_recipient.preference.locale_for_mails) do
        UserMailer.archivist_added_to_collection_notification(
          email_recipient.id,
          item.id,
          collection.id
        ).deliver_later
      end
    end
  end

  before_save :approve_automatically
  def approve_automatically
    return unless self.new_record?

    # approve with the current user, who is the person who has just
    # added this item -- might be either moderator or owner
    # rubocop:disable Lint/BooleanSymbol
    approve(User.current_user == :false ? nil : User.current_user)
    # rubocop:enable Lint/BooleanSymbol

    # if the collection is open or the user who owns this work is a member, go ahead and approve
    # for the collection
    return unless !approved_by_collection? && collection

    approve_by_collection if !collection.moderated? || collection.user_is_maintainer?(User.current_user) || collection.user_is_posting_participant?(User.current_user)
  end

  before_save :send_work_invitation
  def send_work_invitation
    return if approved_by_user? || !approved_by_collection? || !self.new_record? || User.current_user.is_author_of?(item)

    # a maintainer is attempting to add this work to their collection
    # so we send an email to all the works owners
    item.users.each do |email_author|
      next if email_author.preference.collection_emails_off

      I18n.with_locale(email_author.preference.locale_for_mails) do
        UserMailer.invited_to_collection_notification(email_author.id, item.id, collection.id).deliver_now
      end
    end
  end

  after_destroy :expire_caches
  def expire_caches
    if self.item.respond_to?(:expire_caches)
      self.item.expire_caches
      CacheMaster.record(item_id, 'collection', collection_id)
    end
  end

  attr_writer :remove
  def remove
    @remove || ""
  end

  def recipients
    item.respond_to?(:recipients) ? item.recipients : ""
  end

  def item_creator_pseuds
    if self.item
      if self.item.respond_to?(:pseuds)
        self.item.pseuds
      elsif self.item.respond_to?(:pseud)
        [self.item.pseud]
      else
        []
      end
    else
      []
    end
  end

  def item_date
    item.respond_to?(:revised_at) ? item.revised_at : item.updated_at
  end

  def user_allowed_to_destroy?(user)
    user.is_author_of?(self.item) ||
      (self.collection.user_is_maintainer?(user) && !self.rejected_by_user?)
  end

  def approve_by_user
    self.user_approval_status = :approved
  end

  def approve_by_collection
    self.collection_approval_status = :approved
  end

  def approved?
    approved_by_user? && approved_by_collection?
  end

  def approve(user)
    if user.nil?
      # this is being run via rake task eg for importing collections
      approve_by_user
      approve_by_collection
    else
      author_of_item = user.is_author_of?(item) ||
                       (user == User.current_user && item.respond_to?(:pseuds) ? item.pseuds.empty? : item.pseud.nil?)
      archivist_maintainer = user.archivist && self.collection.user_is_maintainer?(user)
      approve_by_user if author_of_item || archivist_maintainer
      approve_by_collection if self.collection.user_is_maintainer?(user)
    end
  end

  def posted?
    self.item.respond_to?(:posted?) ? self.item.posted? : true
  end

  def notify_of_reveal
    unless self.unrevealed? || !self.posted?
      recipient_pseuds = Pseud.parse_bylines(self.recipients)[:pseuds]
      recipient_pseuds.each do |pseud|
        user_preference = pseud.user.preference
        next if user_preference.recipient_emails_off

        I18n.with_locale(user_preference.locale_for_mails) do
          UserMailer.recipient_notification(pseud.user.id, self.item.id, self.collection.id).deliver_after_commit
        end
      end

      # also notify prompters of responses to their prompt
      if item_type == "Work" && !item.challenge_claims.blank?
        users = item.challenge_claims.map { |claim| claim.request_signup.pseud.user }
          .uniq
        users.each do |user|
          I18n.with_locale(user.preference.locale_for_mails) do
            UserMailer.prompter_notification(user.id, self.item.id, self.collection.id).deliver_after_commit
          end
        end
      end

      # also notify the owners of any parent/inspired-by works
      if item_type == "Work" && !item.parent_work_relationships.empty?
        item.parent_work_relationships.each do |relationship|
          relationship.notify_parent_owners
        end
      end
    end
  end

  after_update :notify_of_unrevealed_or_anonymous
  def notify_of_unrevealed_or_anonymous
    # This CollectionItem's anonymous/unrevealed status can only affect the
    # item's status if (a) the CollectionItem is approved by the user and (b)
    # the item is a work. (Bookmarks can't be anonymous/unrevealed at the
    # moment.)
    return unless approved_by_user? && item.is_a?(Work)

    # Check whether anonymous/unrevealed is becoming true, when the work
    # currently has it set to false:
    newly_anonymous = (saved_change_to_anonymous?(to: true) && !item.anonymous?)
    newly_unrevealed = (saved_change_to_unrevealed?(to: true) && !item.unrevealed?)

    return unless newly_unrevealed || newly_anonymous

    # Don't notify if it's one of the work creators who is changing the work's
    # status.
    return if item.users.include?(User.current_user)

    item.users.each do |user|
      I18n.with_locale(user.preference.locale_for_mails) do
        UserMailer.anonymous_or_unrevealed_notification(
          user.id, item.id, collection.id,
          anonymous: newly_anonymous, unrevealed: newly_unrevealed
        ).deliver_after_commit
      end
    end
  end

  # reindex collection after creation, deletion, and approval_status update
  # (we only index approved items, which is why changes there trigger the reindex-index)
  after_commit :update_collection_index, if: :should_update_collection_index?

  def update_collection_index
    ids = [collection_id]
    ids.push(collection.parent_id) if collection.parent.present?
    IndexQueue.enqueue_ids(Collection, ids, :background)
  end

  # reindex collection after creation, deletion, and certain attribute updates
  def should_update_collection_index?
    return true if destroyed?

    pertinent_attributes = %w[collection_approval_status user_approval_status]
    (self.saved_changes.keys & pertinent_attributes).present?
  end
end
