# frozen_string_literal: true

module Dispatch
  module Adapter
    class Claude < Base
      # Thin Net::HTTP wrapper for the Anthropic API.
      #
      # Handles TLS, timeouts, connection-error normalization, and routes
      # non-2xx responses through ClaudeErrors.handle_response!.
      #
      # Two request methods are provided:
      #   post_json(path, body) — buffers the entire response body, returns Hash
      #   get_json(path)        — buffers the entire response body, returns Hash
      #   stream(path, body)    — yields the Net::HTTPResponse for streaming
      #                           (SSE/chunked), still checks status afterwards
      class HttpClient
        # Timeout constants (in seconds)
        OPEN_TIMEOUT   = 30
        READ_TIMEOUT   = 120
        STREAM_TIMEOUT = 300

        # Network-level errors that are normalized to ConnectionError
        NETWORK_ERRORS = [
          Errno::ECONNREFUSED,
          Errno::EHOSTUNREACH,
          Errno::ETIMEDOUT,
          Errno::ECONNRESET,
          Errno::ENETUNREACH,
          Net::OpenTimeout,
          Net::ReadTimeout,
          SocketError,
          IOError,
          OpenSSL::SSL::SSLError
        ].freeze

        # @param base_url     [String]  e.g. "https://api.anthropic.com"
        # @param headers_proc [Proc]    called with no args each request to
        #                               produce a fresh Hash of headers.
        def initialize(base_url:, headers_proc:)
          @uri = URI.parse(base_url.to_s.chomp("/"))
          @headers_proc = headers_proc
        end

        # Perform a POST, return the parsed JSON response body as a Hash.
        #
        # @param path        [String]   e.g. "/v1/messages"
        # @param body        [Hash]     serialized as JSON
        # @param on_response [Proc,nil] called with the raw Net::HTTPResponse
        #                               before the body is parsed (for header capture)
        # @return [Hash]
        def post_json(path, body, on_response: nil)
          headers = build_headers(stream: false)
          request = build_post_request(path, body, headers)
          response = make_request(request, read_timeout: READ_TIMEOUT)
          on_response&.call(response)
          ClaudeErrors.handle_response!(response)
          parse_json_body(response.body)
        end

        # Perform a GET, return the parsed JSON response body as a Hash.
        #
        # @param path        [String]   e.g. "/v1/models"
        # @param on_response [Proc,nil] called with the raw Net::HTTPResponse
        # @return [Hash]
        def get_json(path, on_response: nil)
          headers = build_headers(stream: false)
          request = build_get_request(path, headers)
          response = make_request(request, read_timeout: READ_TIMEOUT)
          on_response&.call(response)
          ClaudeErrors.handle_response!(response)
          parse_json_body(response.body)
        end

        # Perform a streaming POST (SSE / chunked transfer). The
        # `Net::HTTPResponse` is yielded to the caller BEFORE error-checking
        # so that the caller can consume the SSE stream inline. After the
        # block returns, status is still checked (non-2xx → exception).
        #
        # @param path  [String]
        # @param body  [Hash]
        # @yieldparam  response [Net::HTTPResponse]
        # @return [void]
        def stream(path, body, &block)
          raise ArgumentError, "stream requires a block" unless block

          headers = build_headers(stream: true)
          request = build_post_request(path, body, headers)
          do_stream(request, &block)
        end

        private

        # ── Request builders ─────────────────────────────────────────────────

        def build_post_request(path, body, headers)
          req = Net::HTTP::Post.new(full_path(path))
          apply_headers!(req, headers)
          req.body = JSON.generate(body)
          req
        end

        def build_get_request(path, headers)
          req = Net::HTTP::Get.new(full_path(path))
          apply_headers!(req, headers)
          req
        end

        def full_path(path)
          path.to_s.start_with?("/") ? path.to_s : "/#{path}"
        end

        def apply_headers!(request, headers)
          headers.each { |k, v| request[k] = v }
        end

        # ── Connection helpers ───────────────────────────────────────────────

        # Execute a buffered request (full response read into memory).
        def make_request(request, read_timeout:)
          with_connection(read_timeout: read_timeout) do |http|
            http.request(request)
          end
        end

        # Execute a streaming request. The response is yielded with
        # `read_body` available; the block must consume the body before
        # the connection is torn down.
        def do_stream(request, &block)
          with_connection(read_timeout: STREAM_TIMEOUT) do |http|
            http.request(request) do |response|
              block.call(response)
              ClaudeErrors.handle_response!(response)
            end
          end
        end

        # Open an HTTPS connection, yield it, and normalize network errors.
        def with_connection(read_timeout:, &)
          use_ssl  = @uri.scheme == "https"
          hostname = @uri.hostname
          port     = @uri.port || (use_ssl ? 443 : 80)

          http = Net::HTTP.new(hostname, port)
          http.use_ssl      = use_ssl
          http.open_timeout = OPEN_TIMEOUT
          http.read_timeout = read_timeout
          http.verify_mode  = OpenSSL::SSL::VERIFY_PEER if use_ssl

          http.start(&)
        rescue *NETWORK_ERRORS => e
          raise ConnectionError.new(
            "#{ClaudeErrors::PROVIDER}: #{e.class}: #{e.message}",
            provider: ClaudeErrors::PROVIDER
          )
        end

        # ── Header / body helpers ────────────────────────────────────────────

        def build_headers(stream:)
          @headers_proc.call(stream: stream)
        end

        def parse_json_body(body)
          JSON.parse(body.to_s)
        rescue JSON::ParserError => e
          raise RequestError.new(
            "Failed to parse JSON response: #{e.message}",
            provider: ClaudeErrors::PROVIDER
          )
        end
      end
    end
  end
end
