# frozen_string_literal: true

# Use http://tatiyants.com/pev/#/plans/new if you want to optimize a query

task "import:ensure_consistency" => :environment do
  log "Starting..."

  insert_post_timings
  insert_post_replies
  insert_topic_users
  insert_topic_views
  insert_user_actions
  insert_user_options
  insert_user_profiles
  insert_user_stats unless ENV["SKIP_USER_STATS"]
  insert_user_visits
  insert_draft_sequences
  insert_automatic_group_users

  update_user_stats unless ENV["SKIP_USER_STATS"]
  update_posts
  update_topics
  update_categories
  update_users
  update_groups
  update_tag_stats
  update_topic_users
  update_topic_featured_users
  create_category_definitions

  # run_jobs

  log "Done!"
end

MS_SPEND_CREATING_POST = 5000

# -- TODO: We need to check the queries are actually adding/updating the necessary
# data, post migration. The ON CONFLICT DO NOTHING may cause the clauses to be ignored
# when we actually need them to run.

def insert_post_timings
  log "Inserting post timings..."

  DB.exec <<-SQL
    INSERT INTO post_timings (topic_id, post_number, user_id, msecs)
         SELECT topic_id, post_number, user_id, #{MS_SPEND_CREATING_POST}
           FROM posts
          WHERE user_id > 0
    ON CONFLICT DO NOTHING
  SQL
end

def insert_post_replies
  log "Inserting post replies..."

  DB.exec <<-SQL
    INSERT INTO post_replies (post_id, reply_post_id, created_at, updated_at)
         SELECT p2.id, p.id, p.created_at, p.created_at
           FROM posts p
     INNER JOIN posts p2 ON p2.post_number = p.reply_to_post_number AND p2.topic_id = p.topic_id
    ON CONFLICT DO NOTHING
  SQL
end

def insert_topic_users
  log "Inserting topic users..."

  DB.exec <<-SQL
    INSERT INTO topic_users (user_id, topic_id, posted, last_read_post_number, first_visited_at, last_visited_at, total_msecs_viewed)
         SELECT user_id, topic_id, 't' , MAX(post_number), MIN(created_at), MAX(created_at), COUNT(id) * #{MS_SPEND_CREATING_POST}
           FROM posts
          WHERE user_id > 0
       GROUP BY user_id, topic_id
    ON CONFLICT DO NOTHING
  SQL
end

def insert_topic_views
  log "Inserting topic views..."

  DB.exec <<-SQL
    WITH X AS (
          SELECT topic_id, user_id, DATE(p.created_at) posted_at
            FROM posts p
            JOIN users u ON u.id = p.user_id
           WHERE user_id > 0
        GROUP BY topic_id, user_id, DATE(p.created_at)
    )
    INSERT INTO topic_views (topic_id, user_id, viewed_at, ip_address)
         SELECT X.topic_id, X.user_id, X.posted_at, ip_address
           FROM X
           JOIN users u ON u.id = X.user_id
          WHERE ip_address IS NOT NULL
    ON CONFLICT DO NOTHING
  SQL
end

def insert_user_actions
  log "Inserting user actions for NEW_TOPIC = 4..."

  DB.exec <<-SQL
    INSERT INTO user_actions (action_type, user_id, target_topic_id, target_post_id, acting_user_id, created_at, updated_at)
         SELECT 4, p.user_id, topic_id, p.id, p.user_id, p.created_at, p.created_at
           FROM posts p
           JOIN topics t ON t.id = p.topic_id
          WHERE post_number = 1
            AND archetype <> 'private_message'
            AND p.deleted_at IS NULL
            AND t.deleted_at IS NULL
    ON CONFLICT DO NOTHING
  SQL

  log "Inserting user actions for REPLY = 5..."

  DB.exec <<-SQL
    INSERT INTO user_actions (action_type, user_id, target_topic_id, target_post_id, acting_user_id, created_at, updated_at)
         SELECT 5, p.user_id, topic_id, p.id, p.user_id, p.created_at, p.created_at
           FROM posts p
           JOIN topics t ON t.id = p.topic_id
          WHERE post_number > 1
            AND archetype <> 'private_message'
            AND p.deleted_at IS NULL
            AND t.deleted_at IS NULL
    ON CONFLICT DO NOTHING
  SQL

  log "Inserting user actions for RESPONSE = 6..."

  DB.exec <<-SQL
    INSERT INTO user_actions (action_type, user_id, target_topic_id, target_post_id, acting_user_id, created_at, updated_at)
         SELECT 6, p.user_id, p.topic_id, p.id, p2.user_id, p.created_at, p.created_at
           FROM posts p
           JOIN topics t ON t.id = p.topic_id
     INNER JOIN posts p2 ON p2.post_number = p.reply_to_post_number
          WHERE p.post_number > 1
            AND archetype <> 'private_message'
            AND p.deleted_at IS NULL
            AND t.deleted_at IS NULL
            AND p2.topic_id = p.topic_id
            AND p2.user_id <> p.user_id
    ON CONFLICT DO NOTHING
  SQL

  # TODO:
  #  - NEW_PRIVATE_MESSAGE
  #  - GOT_PRIVATE_MESSAGE
end

def insert_user_options
  log "Inserting user options..."

  DB.exec <<-SQL
    INSERT INTO user_options (
                  user_id,
                  mailing_list_mode,
                  mailing_list_mode_frequency,
                  email_level,
                  email_messages_level,
                  email_previous_replies,
                  email_in_reply_to,
                  email_digests,
                  digest_after_minutes,
                  include_tl0_in_digests,
                  automatically_unpin_topics,
                  enable_quoting,
                  enable_smart_lists,
                  external_links_in_new_tab,
                  dynamic_favicon,
                  new_topic_duration_minutes,
                  auto_track_topics_after_msecs,
                  notification_level_when_replying,
                  like_notification_frequency,
                  skip_new_user_tips,
                  hide_profile_and_presence,
                  hide_profile,
                  hide_presence,
                  sidebar_link_to_filtered_list,
                  sidebar_show_count_of_new_items
                )
             SELECT u.id
                  , #{SiteSetting.default_email_mailing_list_mode}
                  , #{SiteSetting.default_email_mailing_list_mode_frequency}
                  , #{SiteSetting.default_email_level}
                  , #{SiteSetting.default_email_messages_level}
                  , #{SiteSetting.default_email_previous_replies}
                  , #{SiteSetting.default_email_in_reply_to}
                  , #{SiteSetting.default_email_digest_frequency.to_i > 0}
                  , #{SiteSetting.default_email_digest_frequency}
                  , #{SiteSetting.default_include_tl0_in_digests}
                  , #{SiteSetting.default_topics_automatic_unpin}
                  , #{SiteSetting.default_other_enable_quoting}
                  , #{SiteSetting.default_other_enable_smart_lists}
                  , #{SiteSetting.default_other_external_links_in_new_tab}
                  , #{SiteSetting.default_other_dynamic_favicon}
                  , #{SiteSetting.default_other_new_topic_duration_minutes}
                  , #{SiteSetting.default_other_auto_track_topics_after_msecs}
                  , #{SiteSetting.default_other_notification_level_when_replying}
                  , #{SiteSetting.default_other_like_notification_frequency}
                  , #{SiteSetting.default_other_skip_new_user_tips}
                  , #{SiteSetting.default_hide_profile || SiteSetting.default_hide_presence}
                  , #{SiteSetting.default_hide_profile}
                  , #{SiteSetting.default_hide_presence}
                  , #{SiteSetting.default_sidebar_link_to_filtered_list}
                  , #{SiteSetting.default_sidebar_show_count_of_new_items}
               FROM users u
          LEFT JOIN user_options uo ON uo.user_id = u.id
              WHERE uo.user_id IS NULL
  SQL
end

def insert_user_profiles
  log "Inserting user profiles..."

  DB.exec <<-SQL
    INSERT INTO user_profiles (user_id)
         SELECT id
           FROM users
    ON CONFLICT DO NOTHING
  SQL
end

def insert_user_stats
  log "Inserting user stats..."

  DB.exec <<~SQL
    INSERT INTO user_stats (user_id, new_since)
    SELECT id, created_at
      FROM users u
     WHERE NOT EXISTS (
       SELECT 1
         FROM user_stats us
        WHERE us.user_id = u.id
     )
        ON CONFLICT DO NOTHING
  SQL
end

def insert_user_visits
  log "Inserting user visits..."

  DB.exec <<-SQL
    INSERT INTO user_visits (user_id, visited_at, posts_read)
         SELECT user_id, DATE(created_at), COUNT(*)
           FROM posts
          WHERE user_id > 0
       GROUP BY user_id, DATE(created_at)
    ON CONFLICT DO NOTHING
  SQL
end

def insert_draft_sequences
  log "Inserting draft sequences..."

  DB.exec <<-SQL
    INSERT INTO draft_sequences (user_id, draft_key, sequence)
         SELECT user_id, CONCAT('#{Draft::EXISTING_TOPIC}', id), 1
           FROM topics
          WHERE user_id > 0
            AND archetype = 'regular'
    ON CONFLICT DO NOTHING
  SQL
end

def insert_automatic_group_users
  Group::AUTO_GROUPS.each do |group_name, group_id|
    user_condition =
      case group_name
      when :everyone
        "TRUE"
      when :admins
        "id > 0 AND admin AND NOT staged"
      when :moderators
        "id > 0 AND moderator AND NOT staged"
      when :staff
        "id > 0 AND (moderator OR admin) AND NOT staged"
      when :trust_level_1, :trust_level_2, :trust_level_3, :trust_level_4
        "id > 0 AND trust_level >= :min_trust_level AND NOT staged"
      when :trust_level_0
        "id > 0 AND NOT staged"
      end

    DB.exec(<<~SQL, group_id: group_id, min_trust_level: group_id - 10)
      INSERT INTO group_users (group_id, user_id, created_at, updated_at)
      SELECT :group_id, id, NOW(), NOW()
        FROM users u
       WHERE #{user_condition}
         AND NOT EXISTS (
                          SELECT 1
                            FROM group_users gu
                           WHERE gu.group_id = :group_id AND gu.user_id = u.id
                        )
    SQL

    Group.reset_user_count(Group.find(group_id))
  end
end

def update_user_stats
  log "Updating user stats..."

  DB.exec <<-SQL
    WITH X AS (
      SELECT p.user_id
           , COUNT(CASE WHEN p.post_number > 1 THEN p.id END) AS posts
           , COUNT(CASE WHEN p.post_number = 1 THEN t.id END) AS topics
           , MIN(p.created_at) min_created_at
           , COALESCE(COUNT(DISTINCT DATE(p.created_at)), 0) days
        FROM posts p
        JOIN topics t ON t.id = p.topic_id
       WHERE p.deleted_at IS NULL
         AND NOT COALESCE(p.hidden, 't')
         AND p.post_type = 1
         AND t.deleted_at IS NULL
         AND COALESCE(t.visible, 't')
         AND t.archetype <> 'private_message'
         AND p.user_id > 0
    GROUP BY p.user_id
    )
    UPDATE user_stats
       SET post_count = X.posts
         , posts_read_count = X.posts
         , time_read = X.posts * 5
         , topic_count = X.topics
         , topics_entered = X.topics
         , first_post_created_at = X.min_created_at
         , days_visited = X.days
      FROM X
     WHERE user_stats.user_id = X.user_id
       AND (post_count <> X.posts
         OR posts_read_count <> X.posts
         OR time_read <> X.posts * 5
         OR topic_count <> X.topics
         OR topics_entered <> X.topics
         OR COALESCE(first_post_created_at, '1970-01-01') <> X.min_created_at
         OR days_visited <> X.days
         )
  SQL
end

def update_posts
  log "Updating post reply counts..."

  DB.exec <<-SQL
    WITH Y AS (
      SELECT post_id, COUNT(*) replies FROM post_replies GROUP BY post_id
    )
    UPDATE posts
       SET reply_count = Y.replies
      FROM Y
     WHERE posts.id = Y.post_id
       AND reply_count <> Y.replies
  SQL

  # -- TODO: ensure this is how this works!
  # WITH X AS (
  #   SELECT pr.post_id, p.user_id
  #     FROM post_replies pr
  #     JOIN posts p ON p.id = pr.reply_post_id
  # )
  # UPDATE posts
  #    SET reply_to_user_id = X.user_id
  #   FROM X
  #  WHERE id = X.post_id
  #    AND COALESCE(reply_to_user_id, -9999) <> X.user_id

  log "Updating post reply_to_user_id..."

  DB.exec <<~SQL
    UPDATE posts AS replies
    SET reply_to_user_id = original.user_id
    FROM posts AS original
    WHERE original.topic_id = replies.topic_id
      AND original.post_number = replies.reply_to_post_number
      AND replies.reply_to_post_number IS NOT NULL
      AND replies.reply_to_user_id IS NULL
      AND replies.post_number <> replies.reply_to_post_number
  SQL
end

def update_topics
  log "Updating topics..."

  DB.exec <<-SQL
    WITH X AS (
      SELECT topic_id
           , COUNT(*) posts
           , MAX(created_at) last_post_date
           , COALESCE(SUM(word_count), 0) words
           , COALESCE(SUM(reply_count), 0) replies
           , (  SELECT user_id
                  FROM posts
                 WHERE NOT hidden
                   AND deleted_at IS NULL
                   AND topic_id = p.topic_id
              ORDER BY post_number DESC
                 LIMIT 1) last_poster
        FROM posts p
       WHERE NOT hidden
         AND deleted_at IS NULL
    GROUP BY topic_id
  )
  UPDATE topics
     SET posts_count = X.posts
       , last_posted_at = X.last_post_date
       , bumped_at = X.last_post_date
       , word_count = X.words
       , reply_count = X.replies
       , last_post_user_id = X.last_poster
    FROM X
   WHERE id = X.topic_id
     AND (posts_count <> X.posts
       OR COALESCE(last_posted_at, '1970-01-01') <> X.last_post_date
       OR bumped_at <> X.last_post_date
       OR COALESCE(word_count, -1) <> X.words
       OR COALESCE(reply_count, -1) <> X.replies
       OR COALESCE(last_post_user_id, -9999) <> X.last_poster)
  SQL
end

def update_categories
  log "Updating categories..."

  DB.exec <<-SQL
    WITH X AS (
        SELECT category_id
             , MAX(p.id) post_id
             , MAX(t.id) topic_id
             , COUNT(p.id) posts
             , COUNT(DISTINCT p.topic_id) topics
          FROM posts p
          JOIN topics t ON t.id = p.topic_id
         WHERE p.deleted_at IS NULL
           AND t.deleted_at IS NULL
           AND NOT p.hidden
           AND t.visible
      GROUP BY category_id
    )
    UPDATE categories
       SET latest_post_id = X.post_id
         , latest_topic_id = X.topic_id
         , post_count = X.posts
         , topic_count = X.topics
      FROM X
     WHERE id = X.category_id
       AND (COALESCE(latest_post_id, -1) <> X.post_id
         OR COALESCE(latest_topic_id, -1) <> X.topic_id
         OR post_count <> X.posts
         OR topic_count <> X.topics)
  SQL
end

def update_users
  log "Updating users..."

  DB.exec(<<~SQL, Archetype.private_message)
    WITH X AS (
        SELECT p.user_id
             , MIN(p.created_at) min_created_at
             , MAX(p.created_at) max_created_at
          FROM posts p
          JOIN topics t ON t.id = p.topic_id AND t.archetype <> ?
         WHERE p.deleted_at IS NULL
      GROUP BY p.user_id
    )
    UPDATE users
       SET first_seen_at  = LEAST(first_seen_at, X.min_created_at)
         , last_seen_at   = GREATEST(last_seen_at, X.max_created_at)
         , last_posted_at = GREATEST(last_posted_at, X.max_created_at)
      FROM X
     WHERE id = X.user_id
       AND (COALESCE(first_seen_at, '1970-01-01')  <> X.min_created_at
         OR COALESCE(last_seen_at, '1970-01-01')   <> X.max_created_at
         OR COALESCE(last_posted_at, '1970-01-01') <> X.max_created_at)
  SQL
end

def update_groups
  log "Updating groups..."

  DB.exec <<-SQL
    WITH X AS (
        SELECT group_id, COUNT(*) count
          FROM group_users
      GROUP BY group_id
    )
    UPDATE groups
       SET user_count = X.count
      FROM X
     WHERE id = X.group_id
       AND user_count <> X.count
  SQL
end

def update_tag_stats
  Tag.ensure_consistency!
end

def update_topic_users
  log "Updating topic users..."

  DB.exec <<-SQL
    WITH X AS (
        SELECT p.topic_id
             , p.user_id
          FROM posts p
          JOIN topics t ON t.id = p.topic_id
         WHERE p.deleted_at IS NULL
           AND t.deleted_at IS NULL
           AND NOT p.hidden
           AND t.visible
    )
    UPDATE topic_users tu
       SET posted = 't'
      FROM X
     WHERE tu.topic_id = X.topic_id
       AND tu.user_id = X.user_id
       AND posted = 'f'
  SQL
end

def update_topic_featured_users
  log "Updating topic featured users..."
  TopicFeaturedUsers.ensure_consistency!
end

def create_category_definitions
  log "Creating category definitions"
  Category.ensure_consistency!
  Site.clear_cache
end

def log(message)
  puts "[#{DateTime.now.strftime("%Y-%m-%d %H:%M:%S")}] #{message}"
end

task "import:create_phpbb_permalinks" => :environment do
  log "Creating Permalinks..."

  # /[^\/]+\/.*-t(\d+).html/
  SiteSetting.permalink_normalizations = '/[^\/]+\/.*-t(\d+).html/thread/\1'

  Topic.listable_topics.find_each do |topic|
    tcf = topic.custom_fields
    if tcf && tcf["import_id"]
      begin
        Permalink.create(url: "thread/#{tcf["import_id"]}", topic_id: topic.id)
      rescue StandardError
        nil
      end
    end
  end

  log "Done!"
end

task "import:remap_old_phpbb_permalinks" => :environment do
  log "Remapping Permalinks..."

  i = 0
  Post
    .where("raw LIKE ?", "%discussions.example.com%")
    .each do |p|
      begin
        new_raw = p.raw.dup
        # \((https?:\/\/discussions\.example\.com\/\S*-t\d+.html)\)
        new_raw.gsub!(%r{\((https?://discussions\.example\.com/\S*-t\d+.html)\)}) do
          normalized_url = Permalink.normalize_url($1)
          permalink =
            begin
              Permalink.find_by_url(normalized_url)
            rescue StandardError
              nil
            end
          if permalink && permalink.target_url
            "(#{permalink.target_url})"
          else
            "(#{$1})"
          end
        end

        if new_raw != p.raw
          p.revise(Discourse.system_user, { raw: new_raw }, bypass_bump: true, skip_revision: true)
          putc "."
          i += 1
        end
      rescue StandardError
        # skip
      end
    end

  log "Done! #{i} posts remapped."
end

task "import:create_vbulletin_permalinks" => :environment do
  log "Creating Permalinks..."

  # /showthread.php\?t=(\d+).*/
  SiteSetting.permalink_normalizations = '/showthread.php\?t=(\d+).*/showthread.php?t=\1'

  Topic.listable_topics.find_each do |topic|
    tcf = topic.custom_fields
    if tcf && tcf["import_id"]
      begin
        Permalink.create(url: "showthread.php?t=#{tcf["import_id"]}", topic_id: topic.id)
      rescue StandardError
        nil
      end
    end
  end

  Category.find_each do |cat|
    ccf = cat.custom_fields
    if ccf && ccf["import_id"]
      begin
        Permalink.create(url: "forumdisplay.php?f=#{ccf["import_id"]}", category_id: cat.id)
      rescue StandardError
        nil
      end
    end
  end

  log "Done!"
end

desc "Import existing exported file"
task "import:file", [:file_name] => [:environment] do |_, args|
  require "import_export"

  ImportExport.import(args[:file_name])
  puts "", "Done", ""
end

desc "Update first_post_created_at column in user_stats table"
task "import:update_first_post_created_at" => :environment do
  log "Updating first_post_created_at..."

  DB.exec <<~SQL
    WITH sub AS (
      SELECT user_id, MIN(posts.created_at) AS first_post_created_at
      FROM posts
      GROUP BY user_id
    )
    UPDATE user_stats
    SET first_post_created_at = sub.first_post_created_at
    FROM user_stats u1
    JOIN sub ON sub.user_id = u1.user_id
    WHERE u1.user_id = user_stats.user_id
      AND user_stats.first_post_created_at IS DISTINCT FROM sub.first_post_created_at
  SQL

  log "Done"
end

desc "Update avatars from external_avatar_url in SSO records"
task "import:update_avatars_from_sso" => :environment do
  log "Updating avatars from SSO records"

  sql = <<~SQL
    SELECT user_id, external_avatar_url
    FROM single_sign_on_records s
    WHERE NOT EXISTS (
      SELECT 1
      FROM user_avatars a
      WHERE a.user_id = s.user_id
    )
  SQL

  queue = SizedQueue.new(1000)
  threads = []

  threads << Thread.new do ||
    DB.query_each(sql) { |row| queue << { user_id: row.user_id, url: row.external_avatar_url } }
    queue.close
  end

  max_count = DB.query_single(<<~SQL).first
    SELECT COUNT(*)
    FROM single_sign_on_records s
    WHERE NOT EXISTS (
      SELECT 1
      FROM user_avatars a
      WHERE a.user_id = s.user_id
    )
  SQL

  status_queue = Queue.new
  status_thread =
    Thread.new do
      error_count = 0
      current_count = 0

      while !(status = status_queue.pop).nil?
        error_count += 1 if !status
        current_count += 1

        print "\r%7d / %7d (%d errors)" % [current_count, max_count, error_count]
      end
    end

  20.times do
    threads << Thread.new do
      while row = queue.pop
        begin
          UserAvatar.import_url_for_user(
            row[:url],
            User.find(row[:user_id]),
            override_gravatar: true,
            skip_rate_limit: true,
          )
          status_queue << true
        rescue StandardError
          status_queue << false
        end
      end
    end
  end

  threads.each(&:join)
  status_queue.close
  status_thread.join
end

def run_jobs
  log "Running jobs"

  Jobs::EnsureDbConsistency.new.execute({})
  Jobs::DirectoryRefreshOlder.new.execute({})
  Jobs::DirectoryRefreshDaily.new.execute({})
  Jobs::ReindexSearch.new.execute({})
  Jobs::TopRefreshToday.new.execute({})
  Jobs::TopRefreshOlder.new.execute({})
  Jobs::Weekly.new.execute({})
end

desc "Rebake posts that contain polls"
task "import:rebake_uncooked_posts_with_polls" => :environment do
  log "Rebaking posts with polls"

  posts = Post.where("EXISTS (SELECT 1 FROM polls WHERE polls.post_id = posts.id)")

  import_rebake_posts(posts)
end

desc "Rebake posts that contain events"
task "import:rebake_uncooked_posts_with_events" => :environment do
  log "Rebaking posts with events"

  posts =
    Post.where(
      "EXISTS (SELECT 1 FROM discourse_post_event_events WHERE discourse_post_event_events.id = posts.id)",
    )

  import_rebake_posts(posts)
end

desc "Rebake posts that have tag"
task "import:rebake_uncooked_posts_with_tag", [:tag_name] => :environment do |_task, args|
  log "Rebaking posts with tag"

  posts =
    Post.where(
      "EXISTS (SELECT 1 FROM topic_tags JOIN tags ON tags.id = topic_tags.tag_id WHERE topic_tags.topic_id = posts.topic_id AND tags.name = ?)",
      args[:tag_name],
    )

  import_rebake_posts(posts)
end

def import_rebake_posts(posts)
  Jobs.run_immediately!
  OptimizedImage.lock_per_machine = false

  posts = posts.where("baked_version <> ? or baked_version IS NULL", Post::BAKED_VERSION)

  max_count = posts.count
  current_count = 0

  ids = posts.pluck(:id)
  # work randomly so you can run this job from lots of consoles if needed
  ids.shuffle!

  ids.each do |id|
    # may have been cooked in interim
    post = posts.where(id: id).first
    post.rebake! if post

    current_count += 1
    print "\r%7d / %7d" % [current_count, max_count]
  end
end