class Creatorship < ApplicationRecord
  belongs_to :pseud
  belongs_to :creation, polymorphic: true, touch: true

  scope :approved, -> { where(approved: true) }
  scope :unapproved, -> { where(approved: false) }

  scope :for_user, ->(user) { joins(:pseud).merge(user.pseuds) }

  ########################################
  # VALIDATIONS
  ########################################

  before_validation :update_approved, on: :create

  validates_presence_of :creation
  validates_uniqueness_of :pseud, scope: [:creation_type, :creation_id], on: :create

  validate :check_invalid, on: :create
  validate :check_banned, on: :create
  validate :check_disallowed, on: :create
  validate :check_approved_becoming_false, on: :update

  # Update approval status if this creatorship should be automatically approved.
  def update_approved
    if !approved? && should_automatically_approve?
      self.approved = true
    end
  end

  # Make sure that the pseud exists, and isn't ambiguous.
  #
  # Note that thanks to the definitions of missing? and ambiguous?, this is
  # equivalent to having a validates_presence_of :pseud check, just with
  # different error messages.
  def check_invalid
    if missing?
      errors.add(:base, ts("Could not find a pseud %{name}.", name: byline))
    elsif ambiguous?
      errors.add(:base, ts("The pseud %{name} is ambiguous.", name: byline))
    end
  end

  # Make sure that the user isn't banned or suspended.
  def check_banned
    return unless pseud&.user&.banned || pseud&.user&.suspended

    errors.add(:base, ts("%{name} cannot be listed as a co-creator.",
                         name: pseud.byline))
    throw :abort
  end

  # Make sure that if this is an invitation, we're not inviting someone who has
  # disabled invitations.
  def check_disallowed
    return if approved? || pseud.nil?
    return if pseud&.user&.preference&.allow_cocreator
    errors.add(:base, ts("%{name} does not allow others to invite them to be a co-creator.",
                         name: pseud.byline))
  end

  # Make sure that we're not trying to set approved to false, since that could
  # potentially violate some rules about co-creators. (e.g. Having a user
  # listed as a chapter co-creator, but not a work co-creator.)
  def check_approved_becoming_false
    if !approved? && approved_changed?
      errors.add(:base, "Once approved, a creatorship cannot become unapproved.")
    end
  end

  ########################################
  # CALLBACKS
  ########################################

  after_create :add_to_parents
  after_update :add_to_parents, if: :saved_change_to_approved?

  before_destroy :expire_caches
  before_destroy :check_not_last
  before_destroy :save_original_creator
  after_destroy :remove_from_children

  after_commit :update_indices

  after_create_commit :notify_creator, if: :enable_notifications

  # If a pseud is listed as a work co-creator (not invited, actually listed),
  # they should also be listed on all of the work's series. Similarly, if a
  # pseud is listed as a chapter co-creator, they should also be listed on the
  # work.
  def add_to_parents
    return unless approved?

    parents = if creation.is_a?(Work)
                creation.series.to_a
              elsif creation.is_a?(Chapter)
                [creation.work]
              else
                []
              end

    parents.each do |parent|
      parent.creatorships.approve_or_create_by(pseud: pseud)
    end
  end

  # In order to make sure that all chapter co-creators are listed on the work,
  # and all work co-creators are listed on the work's series, we need to make
  # sure that when a creatorship is deleted, the deletion cascades downwards.
  def remove_from_children
    # If the creation is being deleted and it's a work, then its chapters are
    # also going to be deleted (which will cause their creatorships to be
    # deleted as well). If the creation is being deleted and it's a series,
    # then we shouldn't delete the work creatorships. So if the creation is
    # being deleted, we don't want to cascade the deletion downwards.
    return if creation.nil? || creation.destroyed?

    children = if creation.is_a?(Work)
                 creation.chapters.to_a
               elsif creation.is_a?(Series)
                 creation.works.to_a
               else
                 []
               end

    children.each do |child|
      child.creatorships.where(pseud: pseud).destroy_all
    end
  end

  # Make sure that both the creation and the pseud are enqueued to be
  # reindexed.
  def update_indices
    if creation.is_a?(Searchable)
      creation.enqueue_to_index
    end

    if pseud && creation.is_a?(Work)
      IndexQueue.enqueue(pseud, :background)
    end
  end

  # Only enable notifications for new creatorships when explicitly enabled.
  attr_accessor :enable_notifications

  # Notify the pseud of their new creatorship.
  def notify_creator
    return unless User.current_user.is_a?(User) &&
                  pseud.user != User.current_user &&
                  pseud.user != User.orphan_account

    I18n.with_locale(pseud.user.preference.locale_for_mails) do
      if approved?
        if User.current_user.try(:is_archivist?)
          UserMailer.creatorship_notification_archivist(id, User.current_user.id).deliver_later
        else
          UserMailer.creatorship_notification(id, User.current_user.id).deliver_later
        end
      else
        UserMailer.creatorship_request(id, User.current_user.id).deliver_later
      end
    end
  end

  # When deleting a creatorship, we want to make sure we're not deleting the
  # very last creatorship for that item.
  def check_not_last
    # We can always delete unapproved creatorships:
    return unless approved?

    # Check that the creation hasn't been deleted, and still has creatorships
    # left:
    return if creation.nil? || creation.destroyed? ||
              creation.creatorships.approved.count > 1

    errors.add(:base, ts("Sorry, we can't remove all creators of a %{type}.",
                         type: creation.model_name.human.downcase))
    raise ActiveRecord::RecordInvalid, self
  end

  # Record the original creator if the creation is a work.
  # This information is stored temporarily to make it available for
  # Policy and Abuse on orphaned works.
  def save_original_creator
    return unless approved?
    return unless creation.is_a?(Work)
    return if creation.destroyed?

    creation.original_creators.create_or_find_by(user: pseud.user).touch
  end

  def expire_caches
    if creation_type == "Work" && self.pseud.present?
      CacheMaster.record(creation_id, "pseud", self.pseud_id)
      CacheMaster.record(creation_id, "user", self.pseud.user_id)
    end
  end

  ########################################
  # OTHER METHODS
  ########################################

  attr_reader :ambiguous_pseuds

  # We define a virtual "byline" attribute to make it easier to handle
  # ambiguous/missing pseuds. By storing the desired name in the @byline
  # variable, we can generate nicely formatted messages.
  def byline=(byline)
    pseuds = Pseud.parse_byline_ambiguous(byline).to_a

    if pseuds.size == 1
      self.pseud = pseuds.first
      @byline = nil
      @ambiguous_pseuds = nil
    else
      self.pseud = nil
      @byline = byline
      @ambiguous_pseuds = pseuds
    end
  end

  # Retrieve the @byline variable, or, failing that, the pseud's byline.
  def byline
    @byline || pseud&.byline
  end

  # A creatorship counts as "missing" if we couldn't find any pseuds matching
  # the passed-in byline.
  def missing?
    pseud.nil? && @ambiguous_pseuds.blank?
  end

  # A creatorship counts as "ambiguous" if there was more than one pseud
  # matching the passed-in byline.
  def ambiguous?
    pseud.nil? && @ambiguous_pseuds.present?
  end

  # Find or initialize a creatorship matching the options, and then set
  # approved to true and save the results. This is a way of adding a new
  # approved creatorship without potentially running into issues with a
  # pre-existing unapproved creatorship.
  def self.approve_or_create_by(options)
    creatorship = find_or_initialize_by(options)
    creatorship.approved = true
    creatorship.save if creatorship.changed?
  end

  # Change authorship of works or series from a particular pseud to the orphan account
  def self.orphan(pseuds, orphans, default=true)
    for pseud in pseuds
      for new_orphan in orphans
        unless pseud.blank? || new_orphan.blank? || !new_orphan.pseuds.include?(pseud)
          orphan_pseud = default ? User.orphan_account.default_pseud : User.orphan_account.pseuds.find_or_create_by(name: pseud.name)
          pseud.change_ownership(new_orphan, orphan_pseud)
        end
      end
    end
  end

  # Calculate whether this creatorship should count as approved, or whether
  # it's just a creatorship invitation.
  def should_automatically_approve?
    # Approve if we're using an API key, or if the current user has special
    # permissions:
    return true if User.current_user.nil? ||
                   pseud&.user == User.current_user ||
                   pseud&.user == User.orphan_account ||
                   User.current_user.try(:is_archivist?)

    # Approve if the creation is a chapter and the pseud is already listed on
    # the work, or if the creation is a series and the pseud is already listed
    # on one of the works:
    (creation.is_a?(Chapter) && creation.work.pseuds.include?(pseud)) ||
      (creation.is_a?(Series) && creation.work_pseuds.include?(pseud))
  end

  # Accept the creatorship invitation. This consists of setting approved to
  # true, and, if the creation is a work, adding the pseud to all of its
  # chapters as well.
  def accept!
    transaction do
      update(approved: true)

      if creation.is_a?(Work)
        creation.chapters.each do |chapter|
          chapter.creatorships.approve_or_create_by(pseud: pseud)
        end
      end
    end
  end
end
