--- HTTP client with retry capability for Kong plugin
-- Wraps resty.http with configurable retry logic for transient failures

local http = require "resty.http"

local HttpClient = {}
HttpClient.__index = HttpClient

-- Default retry configuration
local DEFAULT_MAX_RETRIES = 3
local DEFAULT_RETRY_DELAY = 100  -- milliseconds
local DEFAULT_TIMEOUT = 10000     -- milliseconds

-- HTTP status codes that are retriable (transient errors)
local RETRIABLE_STATUS_CODES = {
  [408] = true,  -- Request Timeout
  [429] = true,  -- Too Many Requests
  [500] = true,  -- Internal Server Error
  [502] = true,  -- Bad Gateway
  [503] = true,  -- Service Unavailable
  [504] = true,  -- Gateway Timeout
}

--- Check if an error is retriable
-- @param err Error message from http request
-- @param status HTTP status code (if available)
-- @return boolean True if the error is retriable
local function is_retriable_error(err, status)
  if status and RETRIABLE_STATUS_CODES[status] then
    return true
  end
  
  -- Network-level errors are retriable
  if err then
    local err_lower = string.lower(err)
    if string.find(err_lower, "timeout") or
       string.find(err_lower, "connection") or
       string.find(err_lower, "refused") or
       string.find(err_lower, "reset") or
       string.find(err_lower, "broken pipe") then
      return true
    end
  end
  
  return false
end

--- Calculate exponential backoff delay
-- @param attempt Current attempt number (0-indexed)
-- @param base_delay Base delay in milliseconds
-- @return number Delay in milliseconds
local function calculate_backoff(attempt, base_delay)
  -- Exponential backoff with jitter: base_delay * 2^attempt + random(0, base_delay)
  local delay = base_delay * math.pow(2, attempt)
  local jitter = math.random(0, base_delay)
  return delay + jitter
end

--- Create a new HTTP client with retry capability
-- @param opts Options table
-- @param opts.max_retries Maximum number of retry attempts (default: 3)
-- @param opts.retry_delay Base delay between retries in milliseconds (default: 100)
-- @param opts.timeout Timeout for each request in milliseconds (default: 10000)
-- @param opts.retry_on_status Additional HTTP status codes to retry on (table)
-- @return table HttpClient instance
function HttpClient.new(opts)
  opts = opts or {}
  
  local self = setmetatable({}, HttpClient)
  
  self.max_retries = opts.max_retries or DEFAULT_MAX_RETRIES
  self.retry_delay = opts.retry_delay or DEFAULT_RETRY_DELAY
  self.timeout = opts.timeout or DEFAULT_TIMEOUT
  
  -- Allow custom retriable status codes
  if opts.retry_on_status then
    self.custom_retriable_status = {}
    for _, status in ipairs(opts.retry_on_status) do
      self.custom_retriable_status[status] = true
    end
  end
  
  return self
end

--- Set timeout for HTTP requests
-- @param timeout Timeout in milliseconds
function HttpClient:set_timeout(timeout)
  self.timeout = timeout
end

--- Check if a status code is retriable (including custom ones)
-- @param status HTTP status code
-- @return boolean True if retriable
function HttpClient:is_status_retriable(status)
  if RETRIABLE_STATUS_CODES[status] then
    return true
  end
  
  if self.custom_retriable_status and self.custom_retriable_status[status] then
    return true
  end
  
  return false
end

--- Execute HTTP request with retry logic
-- @param uri Full request URI
-- @param params Request parameters (method, body, headers, etc.)
-- @return response, error Response object or nil, error message
function HttpClient:request_uri(uri, params)
  params = params or {}
  local method = params.method or "GET"
  local attempt = 0
  
  while attempt <= self.max_retries do
    -- Create a new HTTP client for each attempt
    local httpc = http.new()
    httpc:set_timeout(self.timeout)
    local res, err = httpc:request_uri(uri, params)
    if res and res.status >= 200 and res.status < 300 then
      return res, nil
    end
    
    local should_retry = false
    if attempt < self.max_retries then
      if not res then
        -- Network error
        should_retry = is_retriable_error(err, nil)
      else
        -- HTTP error status
        should_retry = self:is_status_retriable(res.status)
      end
    end
    
    -- Last attempt or non-retriable error
    if not should_retry then
      if res then
        -- Return the error response
        return res, nil
      else
        -- Return the error message
        return nil, err or "Unknown error"
      end
    end
    
    -- Log retry attempt
    local status_info = res and tostring(res.status) or "network error"
    kong.log.warn(string.format(
      "HTTP request failed (retry %d/%d): %s %s - %s, retrying...",
      attempt + 1,
      self.max_retries,
      method,
      uri,
      err or status_info
    ), ", trace_id: ", kong.ctx.shared.trace_id or "-")
    
    -- Calculate backoff delay and sleep
    local delay = calculate_backoff(attempt, self.retry_delay)
    ngx.sleep(delay / 1000)  -- ngx.sleep takes seconds
    
    attempt = attempt + 1
  end
  
  -- Should not reach here, but just in case
  return nil, "Max retries exceeded"
end

--- Expose internal functions for testing
HttpClient._is_retriable_error = is_retriable_error
HttpClient._calculate_backoff = calculate_backoff
HttpClient._RETRIABLE_STATUS_CODES = RETRIABLE_STATUS_CODES

return HttpClient
