# frozen_string_literal: true

##
# From time to time, site admins may choose to sunset a chat channel and archive
# the messages within. The main use case for this is a topic-based channel, but
# it can be used for category channels just fine. It cannot be used for DM channels
# in its current iteration.
#
# To archive a channel, we mark it read_only first to prevent any further message
# additions or changes, and create a record to track whether the archive topic
# will be new or existing. When we archive the channel, messages are copied into
# posts in batches using the [chat] BBCode to quote the messages. The messages are
# deleted once the batch has its post made. The execute action of this class is
# idempotent, so if we fail halfway through the archive process it can be run again.
#
# Once all of the messages have been copied then we mark the channel as archived.
class DiscourseChat::ChatChannelArchiveService
  ARCHIVED_MESSAGES_PER_POST = 100

  def self.begin_archive_process(chat_channel:, acting_user:, topic_params:)
    return if ChatChannelArchive.exists?(chat_channel: chat_channel)

    ChatChannelArchive.transaction do
      chat_channel.read_only!(acting_user)

      archive = ChatChannelArchive.create!(
        chat_channel: chat_channel,
        archived_by: acting_user,
        total_messages: chat_channel.chat_messages.count,
        destination_topic_id: topic_params[:topic_id],
        destination_topic_title: topic_params[:topic_title],
        destination_category_id: topic_params[:category_id],
        destination_tags: topic_params[:tags],
      )
      Jobs.enqueue(:chat_channel_archive, chat_channel_archive_id: archive.id)

      archive
    end
  end

  def self.retry_archive_process(chat_channel:)
    return if !chat_channel.chat_channel_archive&.failed?
    Jobs.enqueue(:chat_channel_archive, chat_channel_archive_id: chat_channel.chat_channel_archive.id)
  end

  attr_reader :chat_channel_archive, :chat_channel, :chat_channel_title

  def initialize(chat_channel_archive)
    @chat_channel_archive = chat_channel_archive
    @chat_channel = chat_channel_archive.chat_channel
    @chat_channel_title = chat_channel.title(chat_channel_archive.archived_by)
  end

  def execute
    chat_channel_archive.update(archive_error: nil)

    begin
      ensure_destination_topic_exists!

      Rails.logger.info("Creating posts from message batches for #{chat_channel_title} archive, #{chat_channel_archive.total_messages} messages to archive (#{chat_channel_archive.total_messages / ARCHIVED_MESSAGES_PER_POST} posts).")

      # a batch should be idempotent, either the post is created and the
      # messages are deleted or we roll back the whole thing.
      #
      # at some point we may want to reconsider disabling post validations,
      # and add in things like dynamic resizing of the number of messages per
      # post based on post length, but that can be done later
      #
      # another future improvement is to send a MessageBus message for each
      # completed batch, so the UI can receive updates and show a progress
      # bar or something similar
      chat_channel.chat_messages.find_in_batches(
        batch_size: ARCHIVED_MESSAGES_PER_POST
      ) do |chat_messages|
        create_post(
          ChatTranscriptService.new(
            chat_channel,
            chat_channel_archive.archived_by,
            messages_or_ids: chat_messages,
            opts: { no_link: true, include_reactions: true }
          ).generate_markdown
        ) do
          delete_message_batch(chat_messages.map(&:id))
        end
      end

      kick_all_users
      complete_archive
    rescue => err
      notify_archiver(:failed, error: err)
      raise err
    end
  end

  private

  def create_post(raw)
    pc = nil
    Post.transaction do
      pc = PostCreator.new(
        Discourse.system_user,
        raw: raw,

        # we must skip these because the posts are created in a big transaction,
        # we do them all at the end instead
        skip_jobs: true,

        # we do not want to be sending out notifications etc. from this
        # automatic background process
        import_mode: true,

        # don't want to be stopped by watched word or post length validations
        skip_validations: true,

        topic_id: chat_channel_archive.destination_topic_id
      )

      pc.create

      # so we can also delete chat messages in the same transaction
      yield if block_given?
    end
    pc.enqueue_jobs
  end

  def ensure_destination_topic_exists!
    if !chat_channel_archive.destination_topic.present?
      Rails.logger.info("Creating topic for #{chat_channel_title} archive.")
      Topic.transaction do
        topic_creator = TopicCreator.new(
          Discourse.system_user,
          Guardian.new(chat_channel_archive.archived_by),
          {
            title: chat_channel_archive.destination_topic_title,
            category: chat_channel_archive.destination_category_id,
            tags: chat_channel_archive.destination_tags,
            import_mode: true
          }
        )

        chat_channel_archive.update!(destination_topic: topic_creator.create)
      end

      Rails.logger.info("Creating first post for #{chat_channel_title} archive.")
      create_post(
        I18n.t(
          "chat.channel.archive.first_post_raw",
          channel_name: chat_channel_title,
          channel_url: chat_channel.url
        )
      )
    else
      Rails.logger.info("Topic already exists for #{chat_channel_title} archive.")
    end

    update_destination_topic_status
  end

  def update_destination_topic_status
    # we only want to do this when the destination topic is new, not an
    # existing topic, because we don't want to update the status unexpectedly
    # on an existing topic
    if chat_channel_archive.destination_topic_title.present?
      if SiteSetting.chat_archive_destination_topic_status == "archived"
        chat_channel_archive.destination_topic.update!(archived: true)
      elsif SiteSetting.chat_archive_destination_topic_status == "closed"
        chat_channel_archive.destination_topic.update!(closed: true)
      end
    end
  end

  def delete_message_batch(message_ids)
    ChatMessage.transaction do
      ChatMessage.where(id: message_ids).update_all(
        deleted_at: DateTime.now,
        deleted_by_id: chat_channel_archive.archived_by.id
      )

      chat_channel_archive.update!(
        archived_messages: chat_channel_archive.archived_messages + message_ids.length
      )
    end

    Rails.logger.info("Archived #{chat_channel_archive.archived_messages} messages for #{chat_channel_title} archive.")
  end

  def complete_archive
    Rails.logger.info("Creating posts completed for #{chat_channel_title} archive.")
    chat_channel.archived!(chat_channel_archive.archived_by)
    notify_archiver(:success)
  end

  def notify_archiver(result, error: nil)
    base_translation_params = {
      channel_name: chat_channel_title,
      topic_title: chat_channel_archive.destination_topic.title,
      topic_url: chat_channel_archive.destination_topic.url
    }

    if result == :failed
      Discourse.warn_exception(
        error,
        message: "Error when archiving chat channel #{chat_channel_title}.",
        env: {
          chat_channel_id: chat_channel.id,
          chat_channel_name: chat_channel_title
        }
      )
      error_translation_params = base_translation_params.merge(
        channel_url: chat_channel.url,
        messages_archived: chat_channel_archive.archived_messages
      )
      chat_channel_archive.update(archive_error: error.message)
      SystemMessage.create_from_system_user(
        chat_channel_archive.archived_by, :chat_channel_archive_failed, error_translation_params
      )
    else
      SystemMessage.create_from_system_user(
        chat_channel_archive.archived_by, :chat_channel_archive_complete, base_translation_params
      )
    end

    ChatPublisher.publish_archive_status(
      chat_channel,
      archive_status: result,
      archived_messages: chat_channel_archive.archived_messages,
      archive_topic_id: chat_channel_archive.destination_topic_id,
      total_messages: chat_channel_archive.total_messages
    )
  end

  def kick_all_users
    UserChatChannelMembership.where(chat_channel: chat_channel).update_all(
      following: false, last_read_message_id: chat_channel.chat_messages.last&.id
    )
  end
end
