# frozen_string_literal: true

# Rename a tag and move everything associated with it. Used when renaming or
# aliasing a tag in a bulk update request. Moves everything associated with the
# tag, including aliases, implications, *_(cosplay) and *_(style) tags, artist
# and wiki pages, saved searches, blacklists, and retags the posts themselves.
class TagMover
  attr_reader :old_tag, :new_tag, :user

  # Initialize a tag move.
  # @param old_name [String] the name of the tag to move
  # @param new_name [String] the new tag name
  # @param user [User] the user to credit for all edits in moving the tag
  def initialize(old_name, new_name, user: User.system)
    @old_tag = Tag.find_or_create_by_name(old_name)
    @new_tag = Tag.find_or_create_by_name(new_name)
    @user = user
  end

  # Perform the tag move.
  def move!
    CurrentUser.scoped(user) do
      move_tag_category!
      move_aliases!
      move_implications!
      move_cosplay_tag!
      move_style_tag!
      move_artist!
      move_wiki!
      move_saved_searches!
      move_blacklists!
      rewrite_wiki_links!
      move_posts!
    end
  end

  # Sync the category of both tags, if one is a general tag and the other is non-general.
  def move_tag_category!
    if old_tag.general? && !new_tag.general?
      old_tag.update!(category: new_tag.category, updater: user)
    elsif new_tag.general? && !old_tag.general?
      new_tag.update!(category: old_tag.category, updater: user)
    end
  end

  # Move the artist entry from the old tag to the new tag, merging it into the
  # new tag's artist entry if it already has an artist entry.
  def move_artist!
    return unless old_tag.artist? && old_artist.present? && !old_artist.is_deleted?

    if new_artist.nil?
      old_artist.name = new_tag.name
      old_artist.other_names += [old_tag.name]
      old_artist.save!(validate: false)
    else
      merge_artists!
    end
  end

  # Move the wiki from the old tag to the new tag, merging it into the new tag's
  # wiki if it already has a wiki.
  def move_wiki!
    return unless old_wiki.present? && !old_wiki.is_deleted?

    if new_wiki.nil?
      old_wiki.update!(title: new_tag.name)
    else
      merge_wikis!
    end
  end

  # Retag the posts from the old tag to the new tag.
  def move_posts!
    old_tag.posts.parallel_find_each do |post|
      DanbooruLogger.info("post ##{post.id}: moving #{old_tag.name} -> #{new_tag.name}")
      post.with_lock do
        post.remove_tag(old_tag.name)
        post.add_tag(new_tag.name)
        post.save! # XXX should do `validate: false`, but validations are currently need to parse the tags
      end
    end
  end

  # Transfer any aliases pointing to the old tag to point to the new tag.
  def move_aliases!
    old_tag.consequent_aliases.each do |tag_alias|
      tag_alias.update!(consequent_name: new_tag.name)
    end
  end

  # Transfer any implications from the old tag to the new tag.
  def move_implications!
    # When renaming A to B, remove any existing A -> ??? implications and
    # replace them with B -> ???, unless they already exist.
    old_tag.antecedent_implications.each do |tag_implication|
      tag_implication.update!(status: :deleted)

      unless TagImplication.active.exists?(antecedent_name: new_tag.name, consequent_name: tag_implication.consequent_name)
        TagImplication.create!(antecedent_name: new_tag.name, consequent_name: tag_implication.consequent_name, creator: user, approver: user, status: :active)
      end
    end

    # When renaming A to B, remove any existing ??? -> A implications and
    # replace them with ??? -> B, unless they already exist.
    old_tag.consequent_implications.each do |tag_implication|
      tag_implication.update!(status: :deleted)

      unless TagImplication.active.exists?(antecedent_name: tag_implication.antecedent_name, consequent_name: new_tag.name)
        TagImplication.create!(antecedent_name: tag_implication.antecedent_name, consequent_name: new_tag.name, creator: user, approver: user, status: :active)
      end
    end
  end

  # Move the character's *_(cosplay) tag if it exists.
  def move_cosplay_tag!
    old_cosplay_tag = "#{old_tag.name}_(cosplay)"
    new_cosplay_tag = "#{new_tag.name}_(cosplay)"

    if Tag.nonempty.exists?(name: old_cosplay_tag)
      TagMover.new(old_cosplay_tag, new_cosplay_tag).move!
    end
  end

  # Move the artist's *_(style) tag if it exists.
  def move_style_tag!
    old_style_tag = "#{old_tag.name}_(style)"
    new_style_tag = "#{new_tag.name}_(style)"

    if old_tag.artist? && Tag.nonempty.exists?(name: old_style_tag)
      TagMover.new(old_style_tag, new_style_tag).move!
    end
  end

  # Update all saved searches to use the new tag.
  def move_saved_searches!
    SavedSearch.rewrite_queries!(old_tag.name, new_tag.name)
  end

  # Update all blacklists to use the new tag.
  def move_blacklists!
    User.rewrite_blacklists!(old_tag.name, new_tag.name)
  end

  # Update any wiki pages or pools linking to the old tag, to link to the new tag.
  def rewrite_wiki_links!
    WikiPage.rewrite_wiki_links!(old_tag.name, new_tag.name)
    Pool.rewrite_wiki_links!(old_tag.name, new_tag.name)
  end

  # Merge two artist entries, copying everything from the old entry to the new
  # one. Duplicate information will be automatically stripped when the artist
  # is saved.
  def merge_artists!
    ApplicationRecord.transaction do
      [old_artist, new_artist].sort_by(&:id).each(&:lock!)

      new_artist.other_names += old_artist.other_names
      new_artist.other_names += [old_artist.name]
      new_artist.group_name = old_artist.group_name unless new_artist.group_name.present?
      new_artist.url_string += "\n" + old_artist.url_string
      new_artist.is_deleted = false
      new_artist.is_banned = old_artist.is_banned || new_artist.is_banned
      new_artist.save!(validate: false)

      old_artist.other_names = [new_artist.name]
      old_artist.group_name = ""
      old_artist.url_string = ""
      old_artist.is_deleted = true
      old_artist.is_banned = false
      old_artist.save!(validate: false)
    end
  end

  # Merge the other names from both wikis. Transfer the body from the old wiki
  # to the new wiki if the new wiki has an empty body. Then mark the old wiki
  # as deleted.
  def merge_wikis!
    ApplicationRecord.transaction do
      [old_wiki, new_wiki].sort_by(&:id).each(&:lock!)

      new_wiki.other_names += old_wiki.other_names
      new_wiki.is_deleted = false
      new_wiki.body = old_wiki.body if new_wiki.body.blank? && old_wiki.body.present?
      new_wiki.save!(validate: false)

      old_wiki.body = "This tag has been moved to [[#{new_wiki.title}]]."
      old_wiki.other_names = []
      old_wiki.is_deleted = true
      old_wiki.save!(validate: false)
    end
  end

  def old_wiki
    old_tag.wiki_page
  end

  def new_wiki
    new_tag.wiki_page
  end

  def old_artist
    old_tag.artist
  end

  def new_artist
    new_tag.artist
  end
end
