# frozen_string_literal: true

module Dispatch
  module Adapter
    class Claude < Base
      module RequestBuilder
        # Converts an Array<Dispatch::Adapter::Message> into the Anthropic
        # messages wire format: [{role:, content:}, …].
        #
        # Key transformations applied here:
        #   - Adjacent "tool result" messages are merged into one user message.
        #   - Thinking block signature rules are applied on assistant messages.
        #   - Image blocks are dropped when the model doesn't accept images.
        #   - Empty text/thinking blocks are elided.
        #   - A trailing assistant message gets a synthetic "Continue." user turn.
        #   - Tool names in tool_use blocks are prefixed with "proxy_" for OAuth.
        #
        # A message is treated as a "tool result message" when its content is an
        # Array exclusively containing ToolResultBlock objects.
        module Messages
          module_function

          # Convert messages to Anthropic wire format.
          #
          # @param messages [Array<Dispatch::Adapter::Message>]
          # @param model_info [Dispatch::Adapter::ModelInfo, nil]
          # @param is_oauth [Boolean]
          # @return [Array<Hash>]
          def build(messages, model_info: nil, is_oauth: false)
            params = []
            i = 0
            while i < messages.length
              msg = messages[i]

              # ── Tool result batching ──────────────────────────────────────────
              if tool_result_message?(msg)
                tool_blocks = []
                while i < messages.length && tool_result_message?(messages[i])
                  tool_blocks.concat(extract_tool_result_blocks(messages[i]))
                  i += 1
                end
                params << { role: "user", content: tool_blocks }
                next
              end

              # ── Normal message roles ──────────────────────────────────────────
              case msg.role.to_s
              when "user", "developer"
                wire = convert_user_message(msg, model_info: model_info)
                params << wire if wire
              when "assistant"
                wire = convert_assistant_message(msg, is_oauth: is_oauth)
                params << wire if wire
              end
              # Any unrecognised roles (e.g. "system") are silently skipped;
              # system prompts are handled separately by the Cloaking module.

              i += 1
            end

            # If the last emitted message is assistant, the API requires a user
            # follow-up (otherwise it returns an error). Use the same synthetic
            # "Continue." that oh-my-pi uses.
            params << { role: "user", content: "Continue." } if params.last&.dig(:role) == "assistant"

            params
          end

          # ── Tool result helpers ─────────────────────────────────────────────

          # Returns true when the message's content is exclusively
          # ToolResultBlock objects (the canonical Ruby wire shape for tool
          # results).
          def tool_result_message?(msg)
            content = msg.content
            return false unless content.is_a?(Array)
            return false if content.empty?

            content.all?(ToolResultBlock)
          end

          # Convert all ToolResultBlock objects in a message to Anthropic
          # tool_result content block hashes.
          def extract_tool_result_blocks(msg)
            msg.content.map do |block|
              wire = {
                type: "tool_result",
                tool_use_id: block.tool_use_id,
                is_error: block.is_error
              }
              converted = convert_tool_result_content(block.content)
              wire[:content] = converted unless converted.nil?
              wire
            end
          end

          # Convert the inner content of a ToolResultBlock to the Anthropic
          # wire shape (String or Array<{type:text|image, …}>).
          def convert_tool_result_content(content)
            case content
            when String
              content.empty? ? nil : content
            when Array
              blocks = content.flat_map { |b| convert_content_for_tool_result(b) }.compact
              blocks.empty? ? nil : blocks
            when nil
              nil
            else
              content.to_s.then { |s| s.empty? ? nil : s }
            end
          end

          def convert_content_for_tool_result(block)
            case block
            when TextBlock
              text = block.text.to_s
              text.empty? ? [] : [{ type: "text", text: text }]
            when ImageBlock
              [build_image_block(block)]
            else
              []
            end
          end

          # ── User / developer messages ────────────────────────────────────────

          def convert_user_message(msg, model_info:)
            case msg.content
            when String
              return nil if msg.content.strip.empty?

              { role: "user", content: msg.content }
            when Array
              blocks = msg.content.flat_map { |b| convert_user_block(b) }.compact

              # Strip image blocks when the model does not support vision
              blocks = blocks.reject { |b| b[:type] == "image" } unless vision_supported?(model_info)

              # Drop empty text blocks
              blocks.reject! { |b| b[:type] == "text" && b[:text].to_s.strip.empty? }

              return nil if blocks.empty?

              { role: "user", content: blocks }
            end
          end

          def convert_user_block(block)
            case block
            when TextBlock
              text = block.text.to_s
              text.empty? ? [] : [{ type: "text", text: text }]
            when ImageBlock
              [build_image_block(block)]
            when ToolResultBlock
              # ToolResultBlock objects within a user message array are handled
              # inline (single message that is already batched). This path is a
              # safety net for mixed-content messages — they are passed through
              # as tool_result blocks inside the user message.
              wire = {
                type: "tool_result",
                tool_use_id: block.tool_use_id,
                is_error: block.is_error
              }
              converted = convert_tool_result_content(block.content)
              wire[:content] = converted unless converted.nil?
              [wire]
            else
              []
            end
          end

          def build_image_block(block)
            {
              type: "image",
              source: {
                type: "base64",
                media_type: block.media_type,
                data: block.source
              }
            }
          end

          def vision_supported?(model_info)
            return true if model_info.nil?

            model_info.supports_vision
          end

          # ── Assistant messages ───────────────────────────────────────────────

          def convert_assistant_message(msg, is_oauth:)
            content = msg.content
            return nil unless content.is_a?(Array)

            # Determine signature policy for thinking blocks in this message.
            # If ANY sibling thinking block is signed, we are in "signed context":
            #   - signed blocks → {type:"thinking", thinking:, signature:}
            #   - unsigned blocks → downgraded to plain text
            # Otherwise (no signed siblings):
            #   - signed blocks → {type:"thinking", thinking:, signature:}
            #   - unsigned blocks → plain text
            has_signed_thinking = content.any? do |b|
              b.is_a?(ThinkingBlock) && !b.signature.to_s.strip.empty?
            end

            blocks = content.flat_map do |block|
              convert_assistant_block(block,
                                      has_signed_thinking: has_signed_thinking,
                                      is_oauth: is_oauth)
            end.compact

            return nil if blocks.empty?

            { role: "assistant", content: blocks }
          end

          def convert_assistant_block(block, has_signed_thinking:, is_oauth:)
            case block
            when TextBlock
              return [] if block.text.to_s.strip.empty?

              [{ type: "text", text: block.text }]
            when ThinkingBlock
              convert_thinking_block(block, has_signed_thinking: has_signed_thinking)
            when RedactedThinkingBlock
              return [] if block.data.to_s.strip.empty?

              [{ type: "redacted_thinking", data: block.data }]
            when ToolUseBlock
              name = is_oauth ? Cloaking.apply_prefix(block.name) : block.name
              [{ type: "tool_use", id: block.id, name: name, input: block.arguments || {} }]
            else
              []
            end
          end

          # Apply the thinking-block signature rules described in research §3.1.
          #
          # Signed context (has_signed_thinking = true):
          #   - Block has non-empty signature → pass through as "thinking"
          #   - Block has no / empty signature → downgrade to plain text
          #     (drop if the thinking text is also empty)
          #
          # Unsigned context (has_signed_thinking = false):
          #   - Block has non-empty signature → pass through as "thinking"
          #   - Block has no signature → plain text (drop if empty)
          def convert_thinking_block(block, has_signed_thinking: false) # rubocop:disable Lint/UnusedMethodArgument
            signed = !block.signature.to_s.strip.empty?

            if signed
              [{ type: "thinking", thinking: block.thinking, signature: block.signature }]
            else
              # Unsigned block: downgrade to text or drop (both signed-context
              # and unsigned-context cases produce identical output).
              return [] if block.thinking.to_s.strip.empty?

              [{ type: "text", text: block.thinking }]
            end
          end
        end
      end
    end
  end
end
