# frozen_string_literal: true

module Dispatch
  module Adapter
    class Claude < Base
      # Parses the `anthropic-ratelimit-unified-*` HTTP response headers that
      # Anthropic sends back on every API call.
      #
      # These headers are the most up-to-date quota signal available — they are
      # attached to every streaming and non-streaming response without any
      # additional requests.  The per-window utilization values are floats in
      # the range 0.0–1.0 (e.g. 0.073 = 7.3% used).
      #
      # Example headers:
      #
      #   anthropic-ratelimit-unified-status: allowed
      #   anthropic-ratelimit-unified-representative-claim: five_hour
      #   anthropic-ratelimit-unified-fallback: available
      #   anthropic-ratelimit-unified-fallback-percentage: 0.5
      #   anthropic-ratelimit-unified-overage-status: rejected
      #   anthropic-ratelimit-unified-5h-status: allowed
      #   anthropic-ratelimit-unified-5h-reset: 1774933200
      #   anthropic-ratelimit-unified-5h-utilization: 0.07
      #   anthropic-ratelimit-unified-5h-surpassed-threshold: false
      #   anthropic-ratelimit-unified-7d-status: allowed
      #   anthropic-ratelimit-unified-7d-utilization: 0.53
      #   anthropic-ratelimit-unified-7d-reset: 1774933200
      #   anthropic-ratelimit-unified-7d_sonnet-status: allowed
      #   anthropic-ratelimit-unified-7d_sonnet-utilization: 0.12
      #   anthropic-ratelimit-unified-7d_opus-utilization: 0.34
      #
      # Reference: reverse-engineered by the community via proxy captures and
      # subsequently confirmed via Anthropic's leaked source map (2026-03-31).
      module RateLimitHeaders
        # Per-window information.
        WindowInfo = Struct.new(
          :window_id,           # String — "5h", "7d", "7d_sonnet", "7d_opus"
          :utilization,         # Float 0.0–1.0 — exact fraction of quota consumed
          :status,              # String — "allowed" | "exceeded" | "rate_limited"
          :reset_at,            # Time | nil — when this window resets
          :surpassed_threshold, # Boolean | nil — true once the quota limit is crossed
          keyword_init: true
        )

        # Top-level summary of all rate-limit windows on a single response.
        Info = Struct.new(
          :status,                # String — overall: "allowed" | "exceeded" | "rate_limited"
          :representative_claim,  # String | nil — which window is the binding constraint
          :fallback,              # String | nil — "available" | "unavailable"
          :fallback_percentage,   # Float | nil — e.g. 0.5
          :overage_status,        # String | nil — "approved" | "rejected"
          :windows,               # Hash{String => WindowInfo} — keyed by window_id
          :captured_at,           # Time — when these headers were read
          :raw_headers,           # Hash{String => String} — all unified headers, for debugging
          keyword_init: true
        ) do
          # Convenience: utilization for the representative (binding) window.
          def binding_utilization
            return nil unless representative_claim

            win = windows[representative_claim] ||
                  windows[representative_claim.tr("-", "_")]
            win&.utilization
          end

          # True iff any window reports exceeded or rate_limited status.
          def limited?
            status == "exceeded" ||
              status == "rate_limited" ||
              windows.any? { |_, w| w.status != "allowed" }
          end

          # Human-readable one-liner for debugging.
          def summary
            parts = windows.map do |wid, w|
              pct = w.utilization ? format("%.1f%%", w.utilization * 100) : "?"
              reset_str = w.reset_at ? " (resets #{w.reset_at.strftime("%H:%M")})" : ""
              "#{wid}: #{pct}#{reset_str} [#{w.status}]"
            end
            parts.unshift("representative=#{representative_claim}") if representative_claim
            parts.unshift("status=#{status}") if status
            parts.join(", ")
          end

          # Serializable hash suitable for JSON logging.
          def to_log_hash
            {
              captured_at: captured_at&.iso8601(3),
              status: status,
              representative_claim: representative_claim,
              fallback: fallback,
              fallback_percentage: fallback_percentage,
              overage_status: overage_status,
              windows: windows.transform_values do |w|
                {
                  utilization: w.utilization,
                  status: w.status,
                  reset_at: w.reset_at&.iso8601,
                  surpassed_threshold: w.surpassed_threshold
                }
              end,
              raw_headers: raw_headers
            }
          end
        end

        # Header prefix for all unified rate-limit headers.
        HEADER_PREFIX = "anthropic-ratelimit-unified-"

        # Known window IDs in the headers.
        WINDOW_IDS = %w[5h 7d 7d_sonnet 7d_opus].freeze

        module_function

        # Parse unified rate-limit headers from a Net::HTTP response.
        #
        # @param response [Net::HTTPResponse]  (or any object that responds to #[])
        # @return [Info, nil]  nil if no unified headers are present
        def parse(response)
          raw = extract_raw_headers(response)
          return nil if raw.empty?

          windows = {}
          WINDOW_IDS.each do |wid|
            win = parse_window(raw, wid)
            windows[wid] = win if win
          end

          Info.new(
            status: raw["status"],
            representative_claim: raw["representative-claim"],
            fallback: raw["fallback"],
            fallback_percentage: raw["fallback-percentage"]&.then(&:to_f),
            overage_status: raw["overage-status"],
            windows: windows,
            captured_at: Time.now,
            raw_headers: raw
          )
        end

        # ── Private helpers ─────────────────────────────────────────────────

        # Extract all headers whose name starts with the unified prefix.
        # Returns a Hash with the prefix stripped from the key.
        def extract_raw_headers(response)
          result = {}
          # Net::HTTPResponse supports each_header (yields lowercase keys)
          if response.respond_to?(:each_header)
            response.each_header do |name, value|
              next unless name.start_with?(HEADER_PREFIX)

              short_key = name[(HEADER_PREFIX.length)..]
              result[short_key] = value
            end
          elsif response.respond_to?(:to_hash)
            # Fallback for stub/mock objects
            response.to_hash.each do |name, values|
              downcased = name.downcase
              next unless downcased.start_with?(HEADER_PREFIX)

              short_key = downcased[(HEADER_PREFIX.length)..]
              result[short_key] = Array(values).first.to_s
            end
          end
          result
        end

        # Parse per-window fields from the raw header map.
        # Window keys use hyphens in headers but we normalise to match WINDOW_IDS
        # which use underscores for compound names (7d_sonnet, 7d_opus).
        def parse_window(raw, wid)
          # Header keys use hyphens; WINDOW_IDS use underscores for compound names.
          hyphen_id = wid.tr("_", "-")

          utilization       = raw["#{hyphen_id}-utilization"]&.then(&:to_f)
          status            = raw["#{hyphen_id}-status"]
          reset_epoch       = raw["#{hyphen_id}-reset"]
          surpassed         = raw["#{hyphen_id}-surpassed-threshold"]

          # Also check underscore form (some headers use them for 7d_sonnet etc.)
          if utilization.nil? && hyphen_id != wid
            utilization   ||= raw["#{wid}-utilization"]&.then(&:to_f)
            status        ||= raw["#{wid}-status"]
            reset_epoch   ||= raw["#{wid}-reset"]
            surpassed     ||= raw["#{wid}-surpassed-threshold"]
          end

          # Skip the window entirely if there are no keys for it.
          return nil if utilization.nil? && status.nil?

          reset_at = reset_epoch ? Time.at(reset_epoch.to_i) : nil
          surpassed_bool = surpassed.nil? ? nil : (surpassed.downcase == "true")

          WindowInfo.new(
            window_id: wid,
            utilization: utilization,
            status: status || "unknown",
            reset_at: reset_at,
            surpassed_threshold: surpassed_bool
          )
        end
      end
    end
  end
end
