# frozen_string_literal: true

##
# From time to time, site admins may choose to sunset a chat channel and archive
# the messages within. 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 Chat::ChatChannelArchiveService
  ARCHIVED_MESSAGES_PER_POST = 100

  class ArchiveValidationError < StandardError
    attr_reader :errors

    def initialize(errors: [])
      super
      @errors = errors
    end
  end

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

    # Only need to validate topic params for a new topic, not an existing one.
    if topic_params[:topic_id].blank?
      valid, errors =
        Chat::ChatChannelArchiveService.validate_topic_params(
          Guardian.new(acting_user),
          topic_params,
        )

      raise ArchiveValidationError.new(errors: errors) if !valid
    end

    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,
    )
    chat_channel.chat_channel_archive
  end

  def self.validate_topic_params(guardian, topic_params)
    topic_creator =
      TopicCreator.new(
        Discourse.system_user,
        guardian,
        {
          title: topic_params[:topic_title],
          category: topic_params[:category_id],
          tags: topic_params[:tags],
          import_mode: true,
        },
      )
    [topic_creator.valid?, topic_creator.errors.full_messages]
  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
      return if !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,
          ) { delete_message_batch(chat_messages.map(&:id)) }
        end

      kick_all_users
      complete_archive
    rescue => err
      notify_archiver(:failed, error_message: err.message)
      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,
            },
          )

        if topic_creator.valid?
          chat_channel_archive.update!(destination_topic: topic_creator.create)
        else
          Rails.logger.info("Destination topic for #{chat_channel_title} archive was not valid.")
          notify_archiver(
            :failed_no_topic,
            error_message: topic_creator.errors.full_messages.join("\n"),
          )
        end
      end

      if chat_channel_archive.destination_topic.present?
        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,
          ),
        )
      end
    else
      Rails.logger.info("Topic already exists for #{chat_channel_title} archive.")
    end

    if chat_channel_archive.destination_topic.present?
      update_destination_topic_status
      return true
    end

    false
  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.new_topic?
      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_message: nil)
    base_translation_params = {
      channel_hashtag_or_name: channel_hashtag_or_name,
      topic_title: chat_channel_archive.destination_topic&.title,
      topic_url: chat_channel_archive.destination_topic&.url,
      topic_validation_errors: result == :failed_no_topic ? error_message : nil,
    }

    if result == :failed || result == :failed_no_topic
      Discourse.warn_exception(
        error_message,
        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)
      message_translation_key =
        case result
        when :failed
          :chat_channel_archive_failed
        when :failed_no_topic
          :chat_channel_archive_failed_no_topic
        end
      SystemMessage.create_from_system_user(
        chat_channel_archive.archived_by,
        message_translation_key,
        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 != :success ? :failed : :success,
      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
    Chat::ChatChannelMembershipManager.new(chat_channel).unfollow_all_users
  end

  def channel_hashtag_or_name
    if chat_channel.slug.present? && SiteSetting.enable_experimental_hashtag_autocomplete
      return "##{chat_channel.slug}::channel"
    end
    chat_channel_title
  end
end
