# typed: strict
class Scraper::CurlHttpPerformer
  extend T::Sig

  # Response =
  #   Struct.new(:response_code, :response_headers, :response_time_ms, :body)

  class Method < T::Enum
    enums do
      Get = new("GET")
      Post = new("POST")
    end
  end

  class Response < T::Struct
    include T::Struct::ActsAsComparable

    const :uri, Addressable::URI
    const :response_code, Integer
    const :response_headers, T::Hash[String, String]
    const :response_time_ms, Integer
    const :body, String
  end

  class Request < T::Struct
    include T::Struct::ActsAsComparable

    const :http_method, Method
    const :uri, Addressable::URI
    const :request_headers, T::Hash[String, String]
    const :body, T.nilable(String)
    const :follow_redirects, T::Boolean, default: true
  end

  sig { returns(String) }
  def name
    "direct"
  end

  sig { params(request: Request).returns(Response) }
  def do_request(request)
    do_request_impl(request)
  end

  private

  sig { params(request: Request).returns(Response) }
  def do_request_impl(request)
    curl = get_curl
    start_at = Time.now
    curl.timeout = 30
    curl.url = request.uri.normalize.to_s
    curl.follow_location = request.follow_redirects
    request.request_headers.each { |key, value| curl.headers[key.to_s] = value }
    curl.headers["User-Agent"] = "FurryArchiver/1.0 / dhelta"
    case request.http_method
    when Method::Get
      curl.get
    when Method::Post
      curl.post_body = request.body
      curl.post
    end
    response_time_ms = ((Time.now - start_at) * 1000).to_i
    response_code = curl.response_code
    body_str = curl.body_str
    response_headers = self.class.parse_header_str(curl.header_str).to_h

    rc_color =
      case response_code / 100
      when 2
        [:green]
      when 3
        [:cyan]
      when 4
        [:red]
      when 5
        %i[red bold]
      else
        [:white]
      end

    rc_string = response_code.to_s
    rc_color.each { |m| rc_string = rc_string.send(m) }

    cf_cache_status =
      if response_headers["cf-cache-status"]
        "(#{response_headers["cf-cache-status"]&.light_blue})"
      else
        nil
      end

    puts [
           "[#{Process.pid.to_s.black.bold} / #{self.name.to_s.black.bold}]",
           "[#{request.http_method.serialize} #{rc_string}",
           "#{self.class.humansize(body_str.size).to_s.bold}",
           (response_time_ms.to_s + "ms").light_blue.bold + "]",
           cf_cache_status,
           "#{request.uri.to_s.bold}",
         ].reject(&:nil?).join(" ")

    Response.new(
      uri: request.uri,
      response_code: response_code,
      response_headers: response_headers,
      response_time_ms: response_time_ms,
      body: body_str,
    )
  end

  sig { returns(Curl::Easy) }
  def get_curl
    t = Thread.current
    unless t.thread_variable?(:curl)
      t.thread_variable_set(:curl, Curl::Easy.new)
    end
    curl = t.thread_variable_get(:curl)
    curl.headers = {}
    curl
  end

  sig { params(size: Integer).returns(String) }
  def self.humansize(size)
    units = %w[B KiB MiB GiB TiB Pib EiB ZiB]
    return "0.0 B" if size == 0
    exp = (Math.log(size) / Math.log(1024)).to_i
    exp += 1 if (size.to_f / 1024**exp >= 1024 - 0.05)
    exp = units.size - 1 if exp > units.size - 1
    unit = units[exp]
    if unit == "B"
      "%d B" % [size]
    else
      "%.1f %s" % [size.to_f / 1024**exp, unit]
    end
  end

  sig { params(header_str: String).returns(T::Array[[String, String]]) }
  def self.parse_header_str(header_str)
    split = header_str.split("\r\n")
    split
      .each_with_index
      .map do |str, idx|
        next nil if idx == 0
        idx = str.index(": ")
        next nil unless idx
        T.cast([str[0...idx], str[idx + 2..-1]], [String, String])
      end
      .compact
  end
end
