# frozen_string_literal: true

require_relative "request_builder/messages"
require_relative "request_builder/tools"
require_relative "request_builder/cache_control"
require_relative "request_builder/thinking"

module Dispatch
  module Adapter
    class Claude < Base
      # Orchestrates all request-builder sub-modules into a single
      # `MessageCreateParamsStreaming`-shaped Hash ready for HTTP serialization.
      #
      # Calling order mirrors oh-my-pi `buildParams`:
      #   1.  Messages.build         — convert interface messages to wire format
      #   2.  Tools.build            — convert tool definitions to wire format
      #   3.  Base body assembly     — model, messages, max_tokens, stream
      #   4.  Sampling params        — drop top_p/top_k on Opus 4.7+
      #   5.  Tool list added        — body[:tools]
      #   6.  tool_choice            — wire-format, apply proxy_ prefix for OAuth
      #   7.  Thinking.apply         — sets thinking / output_config; also runs
      #                                disableThinkingIfToolChoiceForced and
      #                                ensureMaxTokensForThinking
      #   8.  Metadata user_id       — via Cloaking.resolve_user_id
      #   9.  System blocks          — via Cloaking.build_system_blocks (billing
      #                                payload = snapshot of body before system)
      #  10.  CacheControl.apply     — auto-place markers, enforce 4-breakpoint
      #                                cap, normalize TTL ordering
      module RequestBuilder
        module_function

        # Default max_tokens when none is provided: 1/3 of model max, or
        # this value if the pricing table has no entry for the model.
        FALLBACK_MAX_TOKENS         = 8192
        DEFAULT_MAX_TOKENS_DIVISOR  = 3

        # ── Public entry point ────────────────────────────────────────────────

        # Build the complete Anthropic `MessageCreateParamsStreaming` hash.
        #
        # @param model_id [String]
        # @param messages [Array<Dispatch::Adapter::Message>]
        # @param system   [String, Array<Hash>, nil]
        # @param tools    [Array<ToolDefinition, Hash>, nil]
        # @param is_oauth [Boolean]
        # @param base_url [String]
        # @param stream   [Boolean]
        # @param max_tokens [Integer, nil]
        # @param thinking  [String, Hash, nil]
        # @param tool_choice [Symbol, Hash, nil]
        # @param cache_retention [Symbol, nil]  :short | :long | :none | nil
        # @param metadata  [Hash, nil]
        # @param disable_strict_tools [Boolean]
        # @return [Hash]
        def build(
          model_id:,
          messages:,
          system:,
          tools:,
          is_oauth:,
          base_url:,
          stream: true,
          max_tokens: nil,
          thinking: nil,
          tool_choice: nil,
          cache_retention: nil,
          metadata: nil,
          disable_strict_tools: false
        )
          # ── 1. Model info (vision support) ─────────────────────────────────
          model_info = ModelCatalog.build(model_id)

          # ── 2. Messages ────────────────────────────────────────────────────
          model_messages = Messages.build(
            Array(messages),
            model_info: model_info,
            is_oauth: is_oauth
          )

          # ── 3. Tools ───────────────────────────────────────────────────────
          tools_wire = Tools.build(
            Array(tools),
            is_oauth: is_oauth,
            disable_strict: disable_strict_tools
          )

          # ── 4. Base body ───────────────────────────────────────────────────
          body = {
            model: model_id,
            messages: model_messages,
            max_tokens: resolve_max_tokens(max_tokens, model_id),
            stream: stream
          }

          # ── 5. Sampling params — drop top_p / top_k on Opus 4.7+ ──────────
          drop_sampling_if_restricted!(body, model_id)

          # ── 6. Add tools ───────────────────────────────────────────────────
          body[:tools] = tools_wire unless tools_wire.empty?

          # ── 7. tool_choice ─────────────────────────────────────────────────
          wire_tc = build_tool_choice(tool_choice, is_oauth: is_oauth)
          body[:tool_choice] = wire_tc if wire_tc

          # ── 8. Thinking / output_config ────────────────────────────────────
          # Thinking.apply also runs disableThinkingIfToolChoiceForced and
          # ensureMaxTokensForThinking internally.
          Thinking.apply(
            body,
            model_id: model_id,
            thinking: thinking,
            tool_choice: tool_choice,
            max_output_tokens: PricingTable.max_output_tokens(model_id)
          )

          # ── 9. metadata.user_id ────────────────────────────────────────────
          provided_uid = (metadata[:user_id] || metadata["user_id"] if metadata.is_a?(Hash))
          resolved_uid = Cloaking.resolve_user_id(provided_uid, is_oauth)
          body[:metadata] = { user_id: resolved_uid } if resolved_uid

          # ── 10. System blocks ──────────────────────────────────────────────
          # Billing payload = snapshot of body as it stands now (before :system
          # is added), which is what oh-my-pi passes as billingPayload.
          billing_payload = is_oauth ? body.dup : nil
          system_blocks = Cloaking.build_system_blocks(
            system,
            is_oauth: is_oauth,
            model_id: model_id,
            billing_payload: billing_payload
          )
          body[:system] = system_blocks if system_blocks

          # ── 11. Cache-control (also enforces 4-breakpoint cap + TTL order) ─
          CacheControl.apply(body, cache_retention: cache_retention, base_url: base_url)

          body
        end

        # Resolve max_tokens: use caller's value if positive, otherwise derive
        # a sensible default from the pricing table (1/3 of model max).
        def resolve_max_tokens(max_tokens, model_id)
          return max_tokens.to_i if max_tokens.is_a?(Integer) && max_tokens.positive?
          return max_tokens.to_i if max_tokens.is_a?(Numeric) && max_tokens.positive?

          model_max = PricingTable.max_output_tokens(model_id) || FALLBACK_MAX_TOKENS
          divisor   = DEFAULT_MAX_TOKENS_DIVISOR
          derived   = (model_max / divisor).to_i
          derived.positive? ? derived : FALLBACK_MAX_TOKENS
        end

        # Remove top_p / top_k for Opus 4.7+ which rejects non-default
        # sampling parameters with a 400 error.
        def drop_sampling_if_restricted!(body, model_id)
          return unless opus_47_plus?(model_id)

          body.delete(:top_p)
          body.delete(:top_k)
          body.delete("top_p")
          body.delete("top_k")
        end

        # Returns true for claude-opus-4.7+ (major.minor ≥ 4.7).
        # Handles path-prefixed IDs like "anthropic.claude-opus-4-7".
        def opus_47_plus?(model_id)
          id = model_id.to_s
          id = id[(id.rindex("/") + 1)..] if id.include?("/")
          m = /claude-opus-(\d+)[.-](\d+)/.match(id)
          return false unless m

          major = m[1].to_i
          minor = m[2].to_i
          major > 4 || (major == 4 && minor >= 7)
        end

        # Convert the interface's `tool_choice` kwarg to the Anthropic wire format.
        #
        # Interface values:
        #   :auto | :any | :none  →  { type: "auto" } etc.
        #   { type: :tool, name: "bash" }  →  { type: "tool", name: "bash" }
        #     (name is proxy_-prefixed when OAuth)
        #
        # Returns nil when tool_choice is nil or unrecognised.
        def build_tool_choice(tool_choice, is_oauth:)
          case tool_choice
          when Symbol
            { type: tool_choice.to_s }
          when Hash
            h    = tool_choice.transform_keys(&:to_sym)
            type = h[:type].to_s
            name = h[:name]

            wire = { type: type }
            if name
              wire_name = name.to_s
              wire_name = Cloaking.apply_prefix(wire_name) if is_oauth
              wire[:name] = wire_name
            end
            wire
          end
        end
      end
    end
  end
end
