# 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
    @user_id = source_user.id
    @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
    return if @source_user.username == @target_user.username

    if @acting_user
      ::MessageBus.publish "/merge_user",
                           { message: I18n.t("admin.user.merge_user.updating_username") },
                           user_ids: [@acting_user.id]
    end
    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)

    return if posts.count == 0

    if @acting_user
      ::MessageBus.publish "/merge_user",
                           { message: I18n.t("admin.user.merge_user.changing_post_ownership") },
                           user_ids: [@acting_user.id]
    end

    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
    if @acting_user
      ::MessageBus.publish "/merge_user",
                           { message: I18n.t("admin.user.merge_user.merging_given_daily_likes") },
                           user_ids: [@acting_user.id]
    end

    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
    if @acting_user
      ::MessageBus.publish "/merge_user",
                           { message: I18n.t("admin.user.merge_user.merging_post_timings") },
                           user_ids: [@acting_user.id]
    end

    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 = LEAST(t.msecs::bigint + s.msecs, 2^31 - 1)
      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
    if @acting_user
      ::MessageBus.publish "/merge_user",
                           { message: I18n.t("admin.user.merge_user.merging_user_visits") },
                           user_ids: [@acting_user.id]
    end

    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
    if @acting_user
      ::MessageBus.publish "/merge_user",
                           { message: I18n.t("admin.user.merge_user.updating_site_settings") },
                           user_ids: [@acting_user.id]
    end

    SiteSetting
      .all_settings(include_hidden: 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
    if @acting_user
      ::MessageBus.publish "/merge_user",
                           { message: I18n.t("admin.user.merge_user.updating_user_stats") },
                           user_ids: [@acting_user.id]
    end

    # 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
    if @acting_user
      ::MessageBus.publish "/merge_user",
                           { message: I18n.t("admin.user.merge_user.merging_user_attributes") },
                           user_ids: [@acting_user.id]
    end

    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),
        granted_title_badge_id = COALESCE(t.granted_title_badge_id, s.granted_title_badge_id),
        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
    if @acting_user
      ::MessageBus.publish "/merge_user",
                           { message: I18n.t("admin.user.merge_user.updating_user_ids") },
                           user_ids: [@acting_user.id]
    end

    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")

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

    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")

    if @target_user.human?
      update_user_id(
        :user_emails,
        conditions: "x.email = y.email OR y.primary = false",
        updates: '"primary" = false',
      )
    end

    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
    if @acting_user
      ::MessageBus.publish "/merge_user",
                           { message: I18n.t("admin.user.merge_user.deleting_source_user") },
                           user_ids: [@acting_user.id]
    end

    @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