# frozen_string_literal: true

class UserMerger
  def initialize(source_user, target_user, acting_user = nil)
    @source_user = source_user
    @target_user = target_user
    @acting_user = acting_user
    @source_primary_email = source_user.email
  end

  def merge!
    update_username
    move_posts
    update_user_ids
    merge_given_daily_likes
    merge_post_timings
    merge_user_visits
    update_site_settings
    merge_user_attributes

    DiscourseEvent.trigger(:merging_users, @source_user, @target_user)
    update_user_stats

    delete_source_user
    log_merge

    @target_user.reload
  end

  protected

  def update_username
    UsernameChanger.update_username(user_id: @source_user.id,
                                    old_username: @source_user.username,
                                    new_username: @target_user.username,
                                    avatar_template: @target_user.avatar_template,
                                    asynchronous: false)
  end

  def move_posts
    posts = Post.with_deleted
      .where(user_id: @source_user.id)
      .order(:topic_id, :post_number)
      .pluck(:topic_id, :id)

    last_topic_id = nil
    post_ids = []

    posts.each do |current_topic_id, current_post_id|
      if last_topic_id != current_topic_id && post_ids.any?
        change_post_owner(last_topic_id, post_ids)
        post_ids = []
      end

      last_topic_id = current_topic_id
      post_ids << current_post_id
    end

    change_post_owner(last_topic_id, post_ids) if post_ids.any?
  end

  def change_post_owner(topic_id, post_ids)
    PostOwnerChanger.new(
      topic_id: topic_id,
      post_ids: post_ids,
      new_owner: @target_user,
      acting_user: Discourse.system_user,
      skip_revision: true
    ).change_owner!
  end

  def merge_given_daily_likes
    sql = <<~SQL
      INSERT INTO given_daily_likes AS g (user_id, likes_given, given_date, limit_reached)
        SELECT
          :target_user_id                AS user_id,
          COUNT(1)                       AS likes_given,
          a.created_at::DATE             AS given_date,
          COUNT(1) >= :max_likes_per_day AS limit_reached
        FROM post_actions AS a
        WHERE a.user_id = :target_user_id
              AND a.deleted_at IS NULL
              AND EXISTS(
                  SELECT 1
                  FROM given_daily_likes AS g
                  WHERE g.user_id = :source_user_id AND a.created_at::DATE = g.given_date
              )
        GROUP BY given_date
      ON CONFLICT (user_id, given_date)
        DO UPDATE
          SET likes_given = EXCLUDED.likes_given,
            limit_reached = EXCLUDED.limit_reached
    SQL

    DB.exec(
      sql,
      source_user_id: @source_user.id,
      target_user_id: @target_user.id,
      max_likes_per_day: SiteSetting.max_likes_per_day,
      action_type_id: PostActionType.types[:like]
    )
  end

  def merge_post_timings
    update_user_id(:post_timings, conditions: ["x.topic_id = y.topic_id",
                                               "x.post_number = y.post_number"])
    sql = <<~SQL
      UPDATE post_timings AS t
      SET msecs = t.msecs + s.msecs
      FROM post_timings AS s
      WHERE t.user_id = :target_user_id AND s.user_id = :source_user_id
            AND t.topic_id = s.topic_id AND t.post_number = s.post_number
    SQL

    DB.exec(sql, source_user_id: @source_user.id, target_user_id: @target_user.id)
  end

  def merge_user_visits
    update_user_id(:user_visits, conditions: "x.visited_at = y.visited_at")

    sql = <<~SQL
      UPDATE user_visits AS t
      SET posts_read = t.posts_read + s.posts_read,
        mobile       = t.mobile OR s.mobile,
        time_read    = t.time_read + s.time_read
      FROM user_visits AS s
      WHERE t.user_id = :target_user_id AND s.user_id = :source_user_id
            AND t.visited_at = s.visited_at
    SQL

    DB.exec(sql, source_user_id: @source_user.id, target_user_id: @target_user.id)
  end

  def update_site_settings
    SiteSetting.all_settings(true).each do |setting|
      if setting[:type] == "username" && setting[:value] == @source_user.username
        SiteSetting.set_and_log(setting[:setting], @target_user.username)
      end
    end
  end

  def update_user_stats
    # topics_entered
    DB.exec(<<~SQL, target_user_id: @target_user.id)
      UPDATE user_stats
      SET topics_entered = (
        SELECT COUNT(topic_id)
        FROM topic_views
        WHERE user_id = :target_user_id
      )
      WHERE user_id = :target_user_id
    SQL

    # time_read and days_visited
    DB.exec(<<~SQL, target_user_id: @target_user.id)
      UPDATE user_stats
      SET time_read  = COALESCE(x.time_read, 0),
        days_visited = COALESCE(x.days_visited, 0)
      FROM (
             SELECT
               SUM(time_read) AS time_read,
               COUNT(1)       AS days_visited
             FROM user_visits
             WHERE user_id = :target_user_id
           ) AS x
      WHERE user_id = :target_user_id
    SQL

    # posts_read_count
    DB.exec(<<~SQL, target_user_id: @target_user.id)
      UPDATE user_stats
      SET posts_read_count = (
        SELECT COUNT(1)
        FROM post_timings AS pt
        WHERE pt.user_id = :target_user_id AND EXISTS(
            SELECT 1
            FROM topics AS t
            WHERE t.archetype = 'regular' AND t.deleted_at IS NULL
        ))
      WHERE user_id = :target_user_id
    SQL

    # likes_given, likes_received, new_since, read_faq, first_post_created_at
    DB.exec(<<~SQL, source_user_id: @source_user.id, target_user_id: @target_user.id)
      UPDATE user_stats AS t
      SET likes_given         = t.likes_given + s.likes_given,
        likes_received        = t.likes_received + s.likes_received,
        new_since             = LEAST(t.new_since, s.new_since),
        read_faq              = LEAST(t.read_faq, s.read_faq),
        first_post_created_at = LEAST(t.first_post_created_at, s.first_post_created_at)
      FROM user_stats AS s
      WHERE t.user_id = :target_user_id AND s.user_id = :source_user_id
    SQL
  end

  def merge_user_attributes
    DB.exec(<<~SQL, source_user_id: @source_user.id, target_user_id: @target_user.id)
      UPDATE users AS t
      SET created_at              = LEAST(t.created_at, s.created_at),
        updated_at                = LEAST(t.updated_at, s.updated_at),
        seen_notification_id      = GREATEST(t.seen_notification_id, s.seen_notification_id),
        last_posted_at            = GREATEST(t.last_seen_at, s.last_seen_at),
        last_seen_at              = GREATEST(t.last_seen_at, s.last_seen_at),
        admin                     = t.admin OR s.admin,
        last_emailed_at           = GREATEST(t.last_emailed_at, s.last_emailed_at),
        trust_level               = GREATEST(t.trust_level, s.trust_level),
        previous_visit_at         = GREATEST(t.previous_visit_at, s.previous_visit_at),
        date_of_birth             = COALESCE(t.date_of_birth, s.date_of_birth),
        ip_address                = COALESCE(t.ip_address, s.ip_address),
        moderator                 = t.moderator OR s.moderator,
        title                     = COALESCE(t.title, s.title),
        primary_group_id          = COALESCE(t.primary_group_id, s.primary_group_id),
        registration_ip_address   = COALESCE(t.registration_ip_address, s.registration_ip_address),
        first_seen_at             = LEAST(t.first_seen_at, s.first_seen_at),
        manual_locked_trust_level = GREATEST(t.manual_locked_trust_level, s.manual_locked_trust_level)
      FROM users AS s
      WHERE t.id = :target_user_id AND s.id = :source_user_id
    SQL

    DB.exec(<<~SQL, source_user_id: @source_user.id, target_user_id: @target_user.id)
      UPDATE user_profiles AS t
      SET location           = COALESCE(t.location, s.location),
        website              = COALESCE(t.website, s.website),
        bio_raw              = COALESCE(t.bio_raw, s.bio_raw),
        bio_cooked           = COALESCE(t.bio_cooked, s.bio_cooked),
        bio_cooked_version   = COALESCE(t.bio_cooked_version, s.bio_cooked_version),
        profile_background_upload_id = COALESCE(t.profile_background_upload_id, s.profile_background_upload_id),
        dismissed_banner_key = COALESCE(t.dismissed_banner_key, s.dismissed_banner_key),
        badge_granted_title  = t.badge_granted_title OR s.badge_granted_title,
        card_background_upload_id = COALESCE(t.card_background_upload_id, s.card_background_upload_id),
        views                = t.views + s.views
      FROM user_profiles AS s
      WHERE t.user_id = :target_user_id AND s.user_id = :source_user_id
    SQL
  end

  def update_user_ids
    Category.where(user_id: @source_user.id).update_all(user_id: @target_user.id)

    update_user_id(:category_users, conditions: ["x.category_id = y.category_id"])

    update_user_id(:developers)

    update_user_id(:draft_sequences, conditions: "x.draft_key = y.draft_key")
    update_user_id(:drafts, conditions: "x.draft_key = y.draft_key")

    EmailLog.where(user_id: @source_user.id).update_all(user_id: @target_user.id)

    GroupHistory.where(acting_user_id: @source_user.id).update_all(acting_user_id: @target_user.id)
    GroupHistory.where(target_user_id: @source_user.id).update_all(target_user_id: @target_user.id)

    update_user_id(:group_users, conditions: "x.group_id = y.group_id")

    IncomingEmail.where(user_id: @source_user.id).update_all(user_id: @target_user.id)

    IncomingLink.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
    IncomingLink.where(current_user_id: @source_user.id).update_all(current_user_id: @target_user.id)

    InvitedUser.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
    Invite.with_deleted.where(invited_by_id: @source_user.id).update_all(invited_by_id: @target_user.id)
    Invite.with_deleted.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id)

    update_user_id(:muted_users, conditions: "x.muted_user_id = y.muted_user_id")
    update_user_id(:muted_users, user_id_column_name: "muted_user_id", conditions: "x.user_id = y.user_id")

    update_user_id(:ignored_users, conditions: "x.ignored_user_id = y.ignored_user_id")
    update_user_id(:ignored_users, user_id_column_name: "ignored_user_id", conditions: "x.user_id = y.user_id")

    Notification.where(user_id: @source_user.id).update_all(user_id: @target_user.id)

    update_user_id(:post_actions, conditions: ["x.post_id = y.post_id",
                                               "x.post_action_type_id = y.post_action_type_id",
                                               "x.targets_topic = y.targets_topic"])

    PostAction.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id)
    PostAction.where(deferred_by_id: @source_user.id).update_all(deferred_by_id: @target_user.id)
    PostAction.where(agreed_by_id: @source_user.id).update_all(agreed_by_id: @target_user.id)
    PostAction.where(disagreed_by_id: @source_user.id).update_all(disagreed_by_id: @target_user.id)

    PostRevision.where(user_id: @source_user.id).update_all(user_id: @target_user.id)

    Post.with_deleted.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id)
    Post.with_deleted.where(last_editor_id: @source_user.id).update_all(last_editor_id: @target_user.id)
    Post.with_deleted.where(locked_by_id: @source_user.id).update_all(locked_by_id: @target_user.id)
    Post.with_deleted.where(reply_to_user_id: @source_user.id).update_all(reply_to_user_id: @target_user.id)

    Reviewable.where(created_by_id: @source_user.id).update_all(created_by_id: @target_user.id)
    ReviewableHistory.where(created_by_id: @source_user.id).update_all(created_by_id: @target_user.id)

    SearchLog.where(user_id: @source_user.id).update_all(user_id: @target_user.id)

    update_user_id(:tag_users, conditions: "x.tag_id = y.tag_id")

    Theme.where(user_id: @source_user.id).update_all(user_id: @target_user.id)

    update_user_id(:topic_allowed_users, conditions: "x.topic_id = y.topic_id")

    TopicEmbed.with_deleted.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id)

    TopicLink.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
    TopicLinkClick.where(user_id: @source_user.id).update_all(user_id: @target_user.id)

    TopicTimer.with_deleted.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id)

    update_user_id(:topic_timers, conditions: ["x.status_type = y.status_type",
                                               "x.topic_id = y.topic_id",
                                               "y.deleted_at IS NULL"])

    update_user_id(:topic_users, conditions: "x.topic_id = y.topic_id")

    update_user_id(:topic_views, conditions: "x.topic_id = y.topic_id")

    Topic.with_deleted.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id)

    UnsubscribeKey.where(user_id: @source_user.id).update_all(user_id: @target_user.id)

    Upload.where(user_id: @source_user.id).update_all(user_id: @target_user.id)

    update_user_id(:user_archived_messages, conditions: "x.topic_id = y.topic_id")

    update_user_id(:user_actions,
                   user_id_column_name: "user_id",
                   conditions: ["x.action_type = y.action_type",
                                "x.target_topic_id IS NOT DISTINCT FROM y.target_topic_id",
                                "x.target_post_id IS NOT DISTINCT FROM y.target_post_id",
                                "(x.acting_user_id IN (:source_user_id, :target_user_id) OR x.acting_user_id IS NOT DISTINCT FROM y.acting_user_id)"])
    update_user_id(:user_actions,
                   user_id_column_name: "acting_user_id",
                   conditions: ["x.action_type = y.action_type",
                                "x.user_id = y.user_id",
                                "x.target_topic_id IS NOT DISTINCT FROM y.target_topic_id",
                                "x.target_post_id IS NOT DISTINCT FROM y.target_post_id"])

    update_user_id(:user_badges, conditions: ["x.badge_id = y.badge_id",
                                              "x.seq = y.seq",
                                              "x.post_id IS NOT DISTINCT FROM y.post_id"])

    UserBadge.where(granted_by_id: @source_user.id).update_all(granted_by_id: @target_user.id)

    update_user_id(:user_custom_fields, conditions: "x.name = y.name")

    update_user_id(:user_emails, conditions: "x.email = y.email OR y.primary = false", updates: '"primary" = false')

    UserExport.where(user_id: @source_user.id).update_all(user_id: @target_user.id)

    UserHistory.where(target_user_id: @source_user.id).update_all(target_user_id: @target_user.id)
    UserHistory.where(acting_user_id: @source_user.id).update_all(acting_user_id: @target_user.id)

    UserProfileView.where(user_profile_id: @source_user.id).update_all(user_profile_id: @target_user.id)
    UserProfileView.where(user_id: @source_user.id).update_all(user_id: @target_user.id)

    UserWarning.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
    UserWarning.where(created_by_id: @source_user.id).update_all(created_by_id: @target_user.id)

    User.where(approved_by_id: @source_user.id).update_all(approved_by_id: @target_user.id)
  end

  def delete_source_user
    @source_user.reload

    @source_user.skip_email_validation = true
    @source_user.update(
      admin: false,
      email: "#{@source_user.username}_#{SecureRandom.hex}@no-email.invalid"
    )

    UserDestroyer.new(Discourse.system_user).destroy(@source_user, quiet: true)
  end

  def log_merge
    logger = StaffActionLogger.new(@acting_user || Discourse.system_user)
    logger.log_user_merge(@target_user, @source_user.username, @source_primary_email)
  end

  def update_user_id(table_name, opts = {})
    builder = update_user_id_sql_builder(table_name, opts)
    builder.exec(source_user_id: @source_user.id, target_user_id: @target_user.id)
  end

  def update_user_id_sql_builder(table_name, opts = {})
    user_id_column_name = opts[:user_id_column_name] || :user_id
    conditions = Array.wrap(opts[:conditions])
    updates = Array.wrap(opts[:updates])

    builder = DB.build(<<~SQL)
      UPDATE #{table_name} AS x
      /*set*/
      WHERE x.#{user_id_column_name} = :source_user_id AND NOT EXISTS(
          SELECT 1
          FROM #{table_name} AS y
          /*where*/
      )
    SQL

    builder.set("#{user_id_column_name} = :target_user_id")
    updates.each { |u| builder.set(u) }

    builder.where("y.#{user_id_column_name} = :target_user_id")
    conditions.each { |c| builder.where(c) }

    builder
  end
end
