class Collection < ApplicationRecord
  include Filterable
  include WorksOwner
  include Searchable

  has_one_attached :icon do |attachable|
    attachable.variant(:standard, resize_to_limit: [100, 100], loader: { n: -1 })
  end

  # i18n-tasks-use t("errors.attributes.icon.invalid_format")
  # i18n-tasks-use t("errors.attributes.icon.too_large")
  validates :icon, attachment: {
    allowed_formats: %r{image/\S+},
    maximum_size: ArchiveConfig.ICON_SIZE_KB_MAX.kilobytes
  }

  belongs_to :parent, class_name: "Collection", inverse_of: :children
  has_many :children, class_name: "Collection", foreign_key: "parent_id", inverse_of: :parent

  has_one :collection_profile, dependent: :destroy
  accepts_nested_attributes_for :collection_profile

  has_one :collection_preference, dependent: :destroy
  accepts_nested_attributes_for :collection_preference

  before_validation :clear_icon
  before_validation :cleanup_url
  before_create :ensure_associated
  def ensure_associated
    self.collection_preference = CollectionPreference.new unless self.collection_preference
    self.collection_profile = CollectionProfile.new unless self.collection_profile
  end

  belongs_to :challenge, dependent: :destroy, polymorphic: true
  has_many :prompts, dependent: :destroy

  has_many :signups, class_name: "ChallengeSignup", dependent: :destroy
  has_many :potential_matches, dependent: :destroy
  has_many :assignments, class_name: "ChallengeAssignment", dependent: :destroy
  has_many :claims, class_name: "ChallengeClaim", dependent: :destroy

  # We need to get rid of all of these if the challenge is destroyed
  after_save :clean_up_challenge
  def clean_up_challenge
    return if self.challenge_id

    assignments.each(&:destroy)
    potential_matches.each(&:destroy)
    signups.each(&:destroy)
    prompts.each(&:destroy)
  end

  has_many :collection_items, dependent: :destroy
  accepts_nested_attributes_for :collection_items, allow_destroy: true
  has_many :approved_collection_items, -> { approved_by_both }, class_name: "CollectionItem"

  has_many :works, through: :collection_items, source: :item, source_type: "Work"
  has_many :approved_works, -> { posted }, through: :approved_collection_items, source: :item, source_type: "Work"

  has_many :bookmarks, through: :collection_items, source: :item, source_type: "Bookmark"
  has_many :approved_bookmarks, through: :approved_collection_items, source: :item, source_type: "Bookmark"
  has_many :collection_participants, dependent: :destroy
  accepts_nested_attributes_for :collection_participants, allow_destroy: true

  has_many :participants, through: :collection_participants, source: :pseud
  has_many :users, through: :participants, source: :user
  has_many :invited, -> { where(collection_participants: { participant_role: CollectionParticipant::INVITED }) }, through: :collection_participants, source: :pseud
  has_many :owners, -> { where(collection_participants: { participant_role: CollectionParticipant::OWNER }) }, through: :collection_participants, source: :pseud
  has_many :moderators, -> { where(collection_participants: { participant_role: CollectionParticipant::MODERATOR }) }, through: :collection_participants, source: :pseud
  has_many :members, -> { where(collection_participants: { participant_role: CollectionParticipant::MEMBER }) }, through: :collection_participants, source: :pseud
  has_many :posting_participants, -> { where(collection_participants: { participant_role: [CollectionParticipant::MEMBER, CollectionParticipant::MODERATOR, CollectionParticipant::OWNER] }) }, through: :collection_participants, source: :pseud

  CHALLENGE_TYPE_OPTIONS = [
    ["", ""],
    [ts("Gift Exchange"), "GiftExchange"],
    [ts("Prompt Meme"), "PromptMeme"]
  ].freeze

  validate :must_have_owners
  def must_have_owners
    # we have to use collection participants because the association may not exist until after
    # the collection is saved
    errors.add(:base, ts("Collection has no valid owners.")) if (self.collection_participants + (self.parent ? self.parent.collection_participants : [])).select(&:is_owner?)
      .empty?
  end

  validate :collection_depth
  def collection_depth
    errors.add(:base, ts("Sorry, but %{name} is a subcollection, so it can't also be a parent collection.", name: parent.name)) if self.parent&.parent || (self.parent && !self.children.empty?) || (!self.children.empty? && !self.children.collect(&:children).flatten.empty?)
  end

  validate :parent_exists
  def parent_exists
    errors.add(:base, ts("We couldn't find a collection with name %{name}.", name: parent_name)) unless parent_name.blank? || Collection.find_by(name: parent_name)
  end

  validate :parent_is_allowed
  def parent_is_allowed
    if parent
      if parent == self
        errors.add(:base, ts("You can't make a collection its own parent."))
      elsif parent_id_changed? && !parent.user_is_maintainer?(User.current_user)
        errors.add(:base, ts("You have to be a maintainer of %{name} to make a subcollection.", name: parent.name))
      end
    end
  end

  validates :name, presence: { message: ts("Please enter a name for your collection.") }
  validates :name, uniqueness: { message: ts("Sorry, that name is already taken. Try again, please!") }
  validates :name,
            length: { minimum: ArchiveConfig.TITLE_MIN,
                      too_short: ts("must be at least %{min} characters long.", min: ArchiveConfig.TITLE_MIN) }
  validates :name,
            length: { maximum: ArchiveConfig.TITLE_MAX,
                      too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.TITLE_MAX) }
  validates :name,
            format: { message: ts("must begin and end with a letter or number; it may also contain underscores. It may not contain any other characters, including spaces."),
                      with: /\A[A-Za-z0-9]\w*[A-Za-z0-9]\Z/ }
  validates :icon_alt_text, length: { allow_blank: true, maximum: ArchiveConfig.ICON_ALT_MAX,
                                      too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.ICON_ALT_MAX) }
  validates :icon_comment_text, length: { allow_blank: true, maximum: ArchiveConfig.ICON_COMMENT_MAX,
                                          too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.ICON_COMMENT_MAX) }

  validates :email, email_format: { allow_blank: true }

  validates :title, presence: { message: ts("Please enter a title to be displayed for your collection.") }
  validates :title,
            length: { minimum: ArchiveConfig.TITLE_MIN,
                      too_short: ts("must be at least %{min} characters long.", min: ArchiveConfig.TITLE_MIN) }
  validates :title,
            length: { maximum: ArchiveConfig.TITLE_MAX,
                      too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.TITLE_MAX) }
  validate :no_reserved_strings
  def no_reserved_strings
    errors.add(:title, ts("^Sorry, the ',' character cannot be in a collection Display Title.")) if
      title.match(/,/)
  end

  # return title.html_safe to overcome escaping done by sanitiser
  def title
    self[:title].try(:html_safe)
  end

  validates :description,
            length: { allow_blank: true,
                      maximum: ArchiveConfig.SUMMARY_MAX,
                      too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.SUMMARY_MAX) }

  validates :header_image_url, url_format: { allow_blank: true, message: ts("is not a valid URL.") }
  validates :header_image_url, format: { allow_blank: true, with: /\A\S+\.(png|gif|jpe?g)\z/, message: :file_format }

  validates :tags_after_saving,
            length: { maximum: ArchiveConfig.COLLECTION_TAGS_MAX,
                      message: "^Sorry, a collection can only have %{count} tags." }

  scope :top_level, -> { where(parent_id: nil) }
  scope :closed, -> { joins(:collection_preference).where(collection_preferences: { closed: true }) }
  scope :not_closed, -> { joins(:collection_preference).where(collection_preferences: { closed: false }) }
  scope :moderated, -> { joins(:collection_preference).where(collection_preferences: { moderated: true }) }
  scope :unmoderated, -> { joins(:collection_preference).where(collection_preferences: { moderated: false }) }
  scope :unrevealed, -> { joins(:collection_preference).where(collection_preferences: { unrevealed: true }) }
  scope :anonymous, -> { joins(:collection_preference).where(collection_preferences: { anonymous: true }) }
  scope :no_challenge, -> { where(challenge_type: nil) }
  scope :gift_exchange, -> { where(challenge_type: "GiftExchange") }
  scope :prompt_meme, -> { where(challenge_type: "PromptMeme") }
  scope :name_only, -> { select("collections.name") }
  scope :by_title, -> { order(:title) }
  scope :for_blurb, -> { includes(:parent, :moderators, :children, :collection_preference, owners: [:user]).with_attached_icon }
  scope :for_search, -> { includes(:parent, :children, :tags, :challenge, moderators: [user: :pseuds], owners: [user: :pseuds]).with_attached_icon }

  def cleanup_url   
    self.header_image_url = begin 
      Addressable::URI.heuristic_parse(self.header_image_url) if self.header_image_url
    rescue Addressable::URI::InvalidURIError
      self.header_image_url
    end
  end

  # Get only collections with running challenges
  def self.signup_open(challenge_type)
    case challenge_type
    when "PromptMeme"
      not_closed.where(challenge_type: challenge_type)
        .joins("INNER JOIN prompt_memes on prompt_memes.id = challenge_id").where("prompt_memes.signup_open = 1")
        .where("prompt_memes.signups_close_at > ?", Time.zone.now).order("prompt_memes.signups_close_at DESC")
    when "GiftExchange"
      not_closed.where(challenge_type: challenge_type)
        .joins("INNER JOIN gift_exchanges on gift_exchanges.id = challenge_id").where("gift_exchanges.signup_open = 1")
        .where("gift_exchanges.signups_close_at > ?", Time.zone.now).order("gift_exchanges.signups_close_at DESC")
    end
  end

  scope :with_name_like, lambda { |name|
    where("collections.name LIKE ?", "%#{name}%")
      .limit(10)
  }

  scope :with_title_like, lambda { |title|
    where("collections.title LIKE ?", "%#{title}%")
  }

  scope :with_item_count, lambda {
    select("collections.*, count(distinct collection_items.id) as item_count")
      .joins("left join collections child_collections on child_collections.parent_id = collections.id
           left join collection_items on ( (collection_items.collection_id = child_collections.id OR collection_items.collection_id = collections.id)
                                     AND collection_items.user_approval_status = 1
                                     AND collection_items.collection_approval_status = 1)")
      .group("collections.id")
  }

  def to_param
    name_was
  end

  # Change membership of collection(s) from a particular pseud to the orphan account
  def self.orphan(pseuds, collections, default: true)
    pseuds.each do |pseud|
      collections.each do |collection|
        if pseud && collection && collection.owners.include?(pseud)
          orphan_pseud = default ? User.orphan_account.default_pseud : User.orphan_account.pseuds.find_or_create_by(name: pseud.name)
          pseud.change_membership(collection, orphan_pseud)
        end
      end
    end
  end

  ## AUTOCOMPLETE
  # set up autocomplete and override some methods
  include AutocompleteSource

  def autocomplete_search_string
    "#{name} #{title}"
  end

  def autocomplete_search_string_before_last_save
    "#{name_before_last_save} #{title_before_last_save}"
  end

  def autocomplete_prefixes
    ["autocomplete_collection_all",
     "autocomplete_collection_#{closed? ? 'closed' : 'open'}"]
  end

  def autocomplete_score
    all_items.approved_by_collection.approved_by_user.count
  end
  ## END AUTOCOMPLETE

  def parent_name=(name)
    @parent_name = name
    self.parent = Collection.find_by(name: name)
  end

  def parent_name
    @parent_name || (self.parent ? self.parent.name : "")
  end

  def all_owners
    (self.owners + (self.parent ? self.parent.owners : [])).uniq
  end

  def all_moderators
    (self.moderators + (self.parent ? self.parent.moderators : [])).uniq
  end

  def all_members
    (self.members + (self.parent ? self.parent.members : [])).uniq
  end

  def all_posting_participants
    (self.posting_participants + (self.parent ? self.parent.posting_participants : [])).uniq
  end

  def all_participants
    (self.participants + (self.parent ? self.parent.participants : [])).uniq
  end

  def all_items
    CollectionItem.where(collection_id: ([self.id] + self.children.pluck(:id)))
  end

  def maintainers
    self.all_owners + self.all_moderators
  end

  def user_is_owner?(user)
    user && user != false && !(user.pseuds & self.all_owners).empty?
  end

  def user_is_moderator?(user)
    user && user != false && !(user.pseuds & self.all_moderators).empty?
  end

  def user_is_maintainer?(user)
    user && user != false && !(user.pseuds & (self.all_moderators + self.all_owners)).empty?
  end

  def user_is_participant?(user)
    user && user != false && !get_participating_pseuds_for_user(user).empty?
  end

  def user_is_posting_participant?(user)
    user && user != false && !(user.pseuds & self.all_posting_participants).empty?
  end

  def get_participating_pseuds_for_user(user)
    (user && user != false) ? user.pseuds & self.all_participants : []
  end

  def get_participants_for_user(user)
    return [] unless user

    CollectionParticipant.in_collection(self).for_user(user)
  end

  def assignment_notification
    self.collection_profile.assignment_notification || (parent ? parent.collection_profile.assignment_notification : "")
  end

  def gift_notification
    self.collection_profile.gift_notification || (parent ? parent.collection_profile.gift_notification : "")
  end

  def moderated?() = self.collection_preference.moderated

  def closed?() = self.collection_preference.closed

  def unrevealed?() = self.collection_preference.unrevealed

  def anonymous?() = self.collection_preference.anonymous

  def challenge?() = !self.challenge.nil?

  def gift_exchange?
    self.challenge_type == "GiftExchange"
  end

  def prompt_meme?
    self.challenge_type == "PromptMeme"
  end

  def maintainers_list
    self.maintainers.collect(&:user).flatten.uniq
  end

  def collection_email
    return self.email if self.email.present?
    return parent.email if parent && parent.email.present?
  end

  def notify_maintainers_assignments_sent
    subject = I18n.t("user_mailer.collection_notification.assignments_sent.subject")
    message = I18n.t("user_mailer.collection_notification.assignments_sent.complete")
    if self.collection_email.present?
      UserMailer.collection_notification(self.id, subject, message, self.collection_email).deliver_later
    else
      # if collection email is not set and collection parent email is not set, loop through maintainers and send each a notice via email
      self.maintainers_list.each do |user|
        I18n.with_locale(user.preference.locale_for_mails) do
          translated_subject = I18n.t("user_mailer.collection_notification.assignments_sent.subject")
          translated_message = I18n.t("user_mailer.collection_notification.assignments_sent.complete")
          UserMailer.collection_notification(self.id, translated_subject, translated_message, user.email).deliver_later
        end
      end
    end
  end

  def notify_maintainers_challenge_default(challenge_assignment, assignments_page_url)
    if self.collection_email.present?
      subject = I18n.t("user_mailer.collection_notification.challenge_default.subject", offer_byline: challenge_assignment.offer_byline)
      message = I18n.t("user_mailer.collection_notification.challenge_default.complete", offer_byline: challenge_assignment.offer_byline, request_byline: challenge_assignment.request_byline, assignments_page_url: assignments_page_url)
      UserMailer.collection_notification(self.id, subject, message, self.collection_email).deliver_later
    else
      # if collection email is not set and collection parent email is not set, loop through maintainers and send each a notice via email
      self.maintainers_list.each do |user|
        I18n.with_locale(user.preference.locale_for_mails) do
          translated_subject = I18n.t("user_mailer.collection_notification.challenge_default.subject", offer_byline: challenge_assignment.offer_byline)
          translated_message = I18n.t("user_mailer.collection_notification.challenge_default.complete", offer_byline: challenge_assignment.offer_byline, request_byline: challenge_assignment.request_byline, assignments_page_url: assignments_page_url)
          UserMailer.collection_notification(self.id, translated_subject, translated_message, user.email).deliver_later
        end
      end
    end
  end

  include AsyncWithResque
  @queue = :collection

  def reveal!
    async(:reveal_collection_items)
  end

  def reveal_authors!
    async(:reveal_collection_item_authors)
  end

  def reveal_collection_items
    approved_collection_items.each { |collection_item| collection_item.update_attribute(:unrevealed, false) }
    send_reveal_notifications
  end

  def reveal_collection_item_authors
    approved_collection_items.each { |collection_item| collection_item.update_attribute(:anonymous, false) }
  end

  def send_reveal_notifications
    approved_collection_items.each(&:notify_of_reveal)
  end

  # Delete current icon (thus reverting to archive default icon)
  def delete_icon=(value)
    @delete_icon = !value.to_i.zero?
  end

  def delete_icon
    !!@delete_icon
  end
  alias delete_icon? delete_icon

  def clear_icon
    return unless delete_icon?

    self.icon.purge
    self.icon_alt_text = nil
    self.icon_comment_text = nil
  end

  # Work and bookmark counts for indexing; these come from the database.

  def general_works_count
    Work.visible_to_registered_user.in_collection(self).count
  end

  def public_works_count
    Work.visible_to_all.in_collection(self).count
  end

  def general_bookmarked_items_count
    bookmarks = Bookmark.is_public.in_collection(self)

    [
      Work.visible_to_registered_user,
      Series.visible_to_registered_user,
      ExternalWork.visible_to_registered_user
    ].map do |relation|
      relation.joins(:bookmarks).merge(bookmarks).distinct.count("bookmarks.bookmarkable_id")
    end.sum
  end

  def public_bookmarked_items_count
    bookmarks = Bookmark.is_public.in_collection(self)

    [Work.visible_to_all, Series.visible_to_all, ExternalWork.visible_to_all].map do |relation|
      relation.joins(:bookmarks).merge(bookmarks).distinct.count("bookmarks.bookmarkable_id")
    end.sum
  end

  # Work and bookmark counts; these come from Elasticsearch via the
  # SearchCounts helper. It already checks for visibility, so nothing
  # extra needs to be done here.

  def approved_works_count
    SearchCounts.work_count_for_collection(self)
  end

  def approved_bookmarked_items_count
    SearchCounts.bookmarkable_count_for_collection(self)
  end
end
