# :nodoc:
struct OAuth::Signature
  def initialize(@consumer_key : String, @client_shared_secret : String, @oauth_token : String? = nil, @token_shared_secret : String? = nil, @extra_params : Hash(String, String)? = nil)
  end

  def base_string(request : HTTP::Request, tls, ts : String, nonce : String) : String
    base_string request, tls, gather_params(request, ts, nonce)
  end

  def key : String
    String.build do |str|
      URI.encode_www_form @client_shared_secret, str, space_to_plus: false
      str << '&'
      if token_shared_secret = @token_shared_secret
        URI.encode_www_form token_shared_secret, str, space_to_plus: false
      end
    end
  end

  def compute(request : HTTP::Request, tls, ts : String, nonce : String) : String
    base_string = base_string(request, tls, ts, nonce)
    Base64.strict_encode(OpenSSL::HMAC.digest :sha1, key, base_string)
  end

  def authorization_header(request : HTTP::Request, tls, ts : String, nonce : String) : String
    oauth_signature = compute request, tls, ts, nonce

    auth_header = AuthorizationHeader.new
    auth_header.add "oauth_consumer_key", @consumer_key
    auth_header.add "oauth_signature_method", "HMAC-SHA1"
    auth_header.add "oauth_timestamp", ts
    auth_header.add "oauth_nonce", nonce
    auth_header.add "oauth_signature", oauth_signature
    auth_header.add "oauth_token", @oauth_token
    auth_header.add "oauth_version", "1.0"
    @extra_params.try &.each do |key, value|
      auth_header.add key, value
    end
    auth_header.to_s
  end

  private def base_string(request, tls, normalized_params : String)
    host, port = host_and_port(request, tls)

    String.build do |str|
      str << request.method
      str << '&'
      str << (tls ? "https" : "http")
      str << "%3A%2F%2F"
      URI.encode_www_form host, str, space_to_plus: false
      if port
        str << "%3A"
        str << port
      end
      uri_path = request.path.presence || "/"
      URI.encode_www_form(uri_path, str, space_to_plus: false)
      str << '&'
      URI.encode_www_form(normalized_params, str, space_to_plus: false)
    end
  end

  private def gather_params(request, ts, nonce) : String
    params = URI::Params.new

    params.add "oauth_consumer_key", @consumer_key
    params.add "oauth_nonce", nonce
    params.add "oauth_signature_method", "HMAC-SHA1"
    params.add "oauth_timestamp", ts
    if token = @oauth_token
      params.add "oauth_token", token
    end
    params.add "oauth_version", "1.0"

    @extra_params.try &.each do |key, value|
      params.add key, value
    end

    # Inline query parsing
    if query = request.query
      URI::Params.parse(query) do |k, v|
        params.add k, v
      end
    end

    # Inline form body parsing
    body = request.body
    content_type = request.headers["Content-type"]?
    if body && content_type == "application/x-www-form-urlencoded"
      form = body.gets_to_end
      URI::Params.parse(form) do |k, v|
        params.add k, v
      end
      request.body = form
    end

    oauth_normalize_params(params)
  end

  private def host_and_port(request, tls)
    host_header = request.headers["Host"]
    if colon_index = host_header.index ':'
      host = host_header[0...colon_index]
      port = host_header[colon_index + 1..-1].to_i
      {host, port}
    else
      {host_header, nil}
    end
  end

  private def oauth_normalize_params(params : URI::Params) : String
    # Encode names + values first (RFC 5849 §3.4.1.3.2)
    pairs = params.map do |key, value|
      {
        URI.encode_www_form(key.to_s, space_to_plus: false),
        URI.encode_www_form(value.to_s, space_to_plus: false),
      }
    end

    # Sort by encoded key then encoded value
    pairs.sort_by! { |(k, v)| {k, v} }

    # Build final normalized string WITHOUT re-encoding
    String.build do |io|
      pairs.join(io, "&") do |(key, value), io|
        io << key << "=" << value
      end
    end
  end
end
