# frozen_string_literal: true
#
module DiscourseAi
  module Completions
    class PromptMessagesBuilder
      MAX_CHAT_UPLOADS = 5
      MAX_TOPIC_UPLOADS = 5
      attr_reader :chat_context_posts
      attr_accessor :topic

      def self.messages_from_chat(
        message,
        channel:,
        context_post_ids:,
        max_messages:,
        include_uploads:,
        bot_user_ids:,
        instruction_message: nil
      )
        include_thread_titles = !channel.direct_message_channel? && !message.thread_id

        current_id = message.id
        messages = nil

        if !message.thread_id && channel.direct_message_channel?
          messages = [message]
        elsif !channel.direct_message_channel? && !message.thread_id
          messages =
            Chat::Message
              .joins("left join chat_threads on chat_threads.id = chat_messages.thread_id")
              .where(chat_channel_id: channel.id)
              .where(
                "chat_messages.thread_id IS NULL OR chat_threads.original_message_id = chat_messages.id",
              )
              .order(id: :desc)
              .limit(max_messages)
              .to_a
              .reverse
        end

        messages ||=
          ChatSDK::Thread.last_messages(
            thread_id: message.thread_id,
            guardian: Discourse.system_user.guardian,
            page_size: max_messages,
          )

        builder = new

        guardian = Guardian.new(message.user)
        if context_post_ids
          builder.set_chat_context_posts(
            context_post_ids,
            guardian,
            include_uploads: include_uploads,
          )
        end

        messages.each do |m|
          # restore stripped message
          m.message = instruction_message if m.id == current_id && instruction_message

          if bot_user_ids.include?(m.user_id)
            builder.push(type: :model, content: m.message)
          else
            upload_ids = nil
            upload_ids = m.uploads.map(&:id) if include_uploads && m.uploads.present?
            mapped_message = m.message

            thread_title = nil
            thread_title = m.thread&.title if include_thread_titles && m.thread_id
            mapped_message = "(#{thread_title})\n#{m.message}" if thread_title

            if m.uploads.present?
              mapped_message =
                "#{mapped_message} -- uploaded(#{m.uploads.map(&:short_url).join(", ")})"
            end

            builder.push(
              type: :user,
              content: mapped_message,
              id: m.user.username,
              upload_ids: upload_ids,
            )
          end
        end

        builder.to_a(
          limit: max_messages,
          style: channel.direct_message_channel? ? :chat_with_context : :chat,
        )
      end

      def self.messages_from_post(post, style: nil, max_posts:, bot_usernames:, include_uploads:)
        # Pay attention to the `post_number <= ?` here.
        # We want to inject the last post as context because they are translated differently.

        post_types = [Post.types[:regular]]
        post_types << Post.types[:whisper] if post.post_type == Post.types[:whisper]

        context =
          post
            .topic
            .posts
            .joins(:user)
            .joins("LEFT JOIN post_custom_prompts ON post_custom_prompts.post_id = posts.id")
            .where("post_number <= ?", post.post_number)
            .order("post_number desc")
            .where("post_type in (?)", post_types)
            .limit(max_posts)
            .pluck(
              "posts.raw",
              "users.username",
              "post_custom_prompts.custom_prompt",
              "(
                  SELECT array_agg(ref.upload_id)
                  FROM upload_references ref
                  WHERE ref.target_type = 'Post' AND ref.target_id = posts.id
               ) as upload_ids",
              "posts.created_at",
            )

        builder = new
        builder.topic = post.topic

        context.reverse_each do |raw, username, custom_prompt, upload_ids, created_at|
          custom_prompt_translation =
            Proc.new do |message|
              # We can't keep backwards-compatibility for stored functions.
              # Tool syntax requires a tool_call_id which we don't have.
              if message[2] != "function"
                custom_context = {
                  content: message[0],
                  type: message[2].present? ? message[2].to_sym : :model,
                }

                custom_context[:id] = message[1] if custom_context[:type] != :model
                custom_context[:name] = message[3] if message[3]

                thinking = message[4]
                custom_context[:thinking] = thinking if thinking
                custom_context[:created_at] = created_at

                builder.push(**custom_context)
              end
            end

          if custom_prompt.present?
            custom_prompt.each(&custom_prompt_translation)
          else
            context = { content: raw, type: (bot_usernames.include?(username) ? :model : :user) }

            context[:id] = username if context[:type] == :user

            if upload_ids.present? && context[:type] == :user && include_uploads
              context[:upload_ids] = upload_ids.compact
            end
            context[:created_at] = created_at

            builder.push(**context)
          end
        end

        builder.to_a(style: style || (post.topic.private_message? ? :bot : :topic))
      end

      def initialize
        @raw_messages = []
        @timestamps = {}
      end

      def set_chat_context_posts(post_ids, guardian, include_uploads:)
        posts = []
        Post
          .where(id: post_ids)
          .order("id asc")
          .each do |post|
            next if !guardian.can_see?(post)
            posts << post
          end
        if posts.present?
          posts_context = []
          posts_context << "\nThis chat is in the context of the Discourse topic '#{posts[0].topic.title}':\n\n"
          posts_context << "{{{\n"
          posts.each do |post|
            posts_context << "url: #{post.url}\n"
            posts_context << "#{post.username}: #{post.raw}\n\n"
            if include_uploads
              post.uploads.each { |upload| posts_context << { upload_id: upload.id } }
            end
          end
          posts_context << "}}}"
          @chat_context_posts = posts_context
        end
      end

      def to_a(limit: nil, style: nil)
        # topic and chat array are special, they are single messages that contain all history
        return chat_array(limit: limit) if style == :chat
        return topic_array if style == :topic

        # the rest of the styles can include multiple messages
        result = valid_messages_array(@raw_messages)
        prepend_chat_post_context(result) if style == :chat_with_context

        if limit
          result[0..limit]
        else
          result
        end
      end

      def push(type:, content:, name: nil, upload_ids: nil, id: nil, thinking: nil, created_at: nil)
        if !%i[user model tool tool_call system].include?(type)
          raise ArgumentError, "type must be either :user, :model, :tool, :tool_call or :system"
        end
        raise ArgumentError, "upload_ids must be an array" if upload_ids && !upload_ids.is_a?(Array)

        content = [content, *upload_ids.map { |upload_id| { upload_id: upload_id } }] if upload_ids
        message = { type: type, content: content }
        message[:name] = name.to_s if name
        message[:id] = id.to_s if id
        if thinking
          message[:thinking] = thinking["thinking"] if thinking["thinking"]
          message[:thinking_signature] = thinking["thinking_signature"] if thinking[
            "thinking_signature"
          ]
          message[:redacted_thinking_signature] = thinking[
            "redacted_thinking_signature"
          ] if thinking["redacted_thinking_signature"]
        end

        @raw_messages << message
        @timestamps[message] = created_at if created_at

        message
      end

      private

      def valid_messages_array(messages)
        result = []

        # this will create a "valid" messages array
        # 1. ensures we always start with a user message
        # 2. ensures we always end with a user message
        # 3. ensures we always interleave user and model messages
        last_type = nil
        messages.each do |message|
          next if !last_type && message[:type] != :user

          if last_type == :tool_call && message[:type] != :tool
            result.pop
            last_type = result.length > 0 ? result[-1][:type] : nil
          end

          next if message[:type] == :tool && last_type != :tool_call

          if message[:type] == last_type
            # merge the message for :user message
            # replace the message for other messages
            last_message = result[-1]

            if message[:type] == :user
              old_name = last_message.delete(:id)
              last_message[:content] = ["#{old_name}: ", last_message[:content]].flatten if old_name

              new_content = message[:content]
              new_content = ["#{message[:id]}: ", new_content].flatten if message[:id]

              if !last_message[:content].is_a?(Array)
                last_message[:content] = [last_message[:content]]
              end
              last_message[:content].concat(["\n", new_content].flatten)

              compressed =
                compress_messages_buffer(last_message[:content], max_uploads: MAX_TOPIC_UPLOADS)
              last_message[:content] = compressed
            else
              last_message[:content] = message[:content]
            end
          else
            result << message
          end

          last_type = message[:type]
        end

        result
      end

      def prepend_chat_post_context(messages)
        return if @chat_context_posts.blank?

        old_content = messages[0][:content]
        old_content = [old_content] if !old_content.is_a?(Array)

        new_content = []
        new_content << "You are replying inside a Discourse chat.\n"
        new_content.concat(@chat_context_posts)
        new_content << "\n"
        new_content << "Your instructions are:\n"
        new_content.concat(old_content)

        compressed = compress_messages_buffer(new_content.flatten, max_uploads: MAX_CHAT_UPLOADS)

        messages[0][:content] = compressed
      end

      def format_user_info(user)
        info = []
        info << user_role(user)
        info << "Trust level #{user.trust_level}" if user.trust_level > 0
        info << "#{account_age(user)}"
        info << "#{user.user_stat.post_count} posts" if user.user_stat.post_count.to_i > 0
        "#{user.username} (#{user.name}): #{info.compact.join(", ")}"
      end

      def format_timestamp(timestamp)
        return nil unless timestamp

        time_diff = Time.now - timestamp

        if time_diff < 1.minute
          "just now"
        elsif time_diff < 1.hour
          mins = (time_diff / 1.minute).round
          "#{mins} #{mins == 1 ? "minute" : "minutes"} ago"
        elsif time_diff < 1.day
          hours = (time_diff / 1.hour).round
          "#{hours} #{hours == 1 ? "hour" : "hours"} ago"
        elsif time_diff < 7.days
          days = (time_diff / 1.day).round
          "#{days} #{days == 1 ? "day" : "days"} ago"
        elsif time_diff < 30.days
          weeks = (time_diff / 7.days).round
          "#{weeks} #{weeks == 1 ? "week" : "weeks"} ago"
        elsif time_diff < 365.days
          months = (time_diff / 30.days).round
          "#{months} #{months == 1 ? "month" : "months"} ago"
        else
          years = (time_diff / 365.days).round
          "#{years} #{years == 1 ? "year" : "years"} ago"
        end
      end

      def user_role(user)
        return "moderator" if user.moderator?
        return "admin" if user.admin?
        nil
      end

      def account_age(user)
        years = ((Time.now - user.created_at) / 1.year).round
        months = ((Time.now - user.created_at) / 1.month).round % 12

        output = []
        if years > 0
          output << years.to_s
          output << "year" if years == 1
          output << "years" if years > 1
        end
        if months > 0
          output << months.to_s
          output << "month" if months == 1
          output << "months" if months > 1
        end

        if output.empty?
          "new account"
        else
          "account age: " + output.join(" ")
        end
      end

      def format_topic_info(topic)
        content_array = []

        if topic.private_message?
          content_array << "Private message info.\n"
        else
          content_array << "Topic information:\n"
        end

        content_array << "- URL: #{topic.url}\n"
        content_array << "- Title: #{topic.title}\n"
        if SiteSetting.tagging_enabled
          tags = topic.tags.pluck(:name)
          tags -= DiscourseTagging.hidden_tag_names if tags.present?
          content_array << "- Tags: #{tags.join(", ")}\n" if tags.present?
        end
        if !topic.private_message?
          content_array << "- Category: #{topic.category.name}\n" if topic.category
        end
        content_array << "- Number of replies: #{topic.posts_count - 1}\n\n"

        content_array.join
      end

      def format_user_infos(usernames)
        content_array = []

        if usernames.present?
          users_details =
            User
              .where(username: usernames)
              .includes(:user_stat)
              .map { |user| format_user_info(user) }
              .compact
          content_array << "User information:\n"
          content_array << "- #{users_details.join("\n- ")}\n\n" if users_details.present?
        end
        content_array.join
      end

      def topic_array
        raw_messages = @raw_messages.dup
        content_array = []
        content_array << "You are operating in a Discourse forum.\n\n"
        content_array << format_topic_info(@topic) if @topic

        if raw_messages.present?
          usernames =
            raw_messages.filter { |message| message[:type] == :user }.map { |message| message[:id] }

          content_array << format_user_infos(usernames) if usernames.present?
        end

        last_user_message = raw_messages.pop

        if raw_messages.present?
          content_array << "Here is the conversation so far:\n"
          raw_messages.each do |message|
            content_array << "#{message[:id] || "User"}: "
            timestamp = @timestamps[message]
            content_array << "(#{format_timestamp(timestamp)}) " if timestamp
            content_array << message[:content]
            content_array << "\n\n"
          end
        end

        if last_user_message
          content_array << "Latest post is by #{last_user_message[:id] || "User"} who just posted:\n"
          content_array << last_user_message[:content]
        end

        content_array =
          compress_messages_buffer(content_array.flatten, max_uploads: MAX_TOPIC_UPLOADS)

        user_message = { type: :user, content: content_array }

        [user_message]
      end

      def chat_array(limit:)
        if @raw_messages.length > 1
          buffer = [
            +"You are replying inside a Discourse chat channel. Here is a summary of the conversation so far:\n{{{",
          ]

          @raw_messages[0..-2].each do |message|
            buffer << "\n"

            if message[:type] == :user
              buffer << "#{message[:id] || "User"}: "
            else
              buffer << "Bot: "
            end

            buffer << message[:content]
          end

          buffer << "\n}}}"
          buffer << "\n\n"
          buffer << "Your instructions:"
          buffer << "\n"
        end

        last_message = @raw_messages[-1]
        buffer << "#{last_message[:id] || "User"}: "
        buffer << last_message[:content]

        buffer = compress_messages_buffer(buffer.flatten, max_uploads: MAX_CHAT_UPLOADS)

        message = { type: :user, content: buffer }
        [message]
      end

      # caps uploads to maximum uploads allowed in message stream
      # and concats string elements
      def compress_messages_buffer(buffer, max_uploads:)
        compressed = []
        current_text = +""
        upload_count = 0

        buffer.each do |item|
          if item.is_a?(String)
            current_text << item
          elsif item.is_a?(Hash)
            compressed << current_text if current_text.present?
            compressed << item
            current_text = +""
            upload_count += 1
          end
        end

        compressed << current_text if current_text.present?

        if upload_count > max_uploads
          to_remove = upload_count - max_uploads
          removed = 0
          compressed.delete_if { |item| item.is_a?(Hash) && (removed += 1) <= to_remove }
        end

        compressed = compressed[0] if compressed.length == 1 && compressed[0].is_a?(String)

        compressed
      end
    end
  end
end
