local B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

local function base64encode(s)
  local pad = (3 - #s % 3) % 3;
  s = s .. string.rep('\0', pad);
  local r = {};
  for i = 1, #s, 3 do
    local a, b, c = s:byte(i), s:byte(i + 1), s:byte(i + 2);
    local n = a * 65536 + b * 256 + c;
    r[#r + 1] = B64:sub(math.floor(n / 262144) % 64 + 1, math.floor(n / 262144) % 64 + 1)
             .. B64:sub(math.floor(n / 4096) % 64 + 1, math.floor(n / 4096) % 64 + 1)
             .. B64:sub(math.floor(n / 64) % 64 + 1, math.floor(n / 64) % 64 + 1)
             .. B64:sub(n % 64 + 1, n % 64 + 1);
  end
  local result = table.concat(r);
  if pad > 0 then
    result = result:sub(1, #result - pad) .. string.rep('=', pad);
  end
  return result;
end

local function trimTrailingSlash(s)
  return (s:gsub('/+$', ''));
end

local function urlEncode(s)
  return (tostring(s):gsub('[^%w%-_%.~]', function(c)
    return string.format('%%%02X', string.byte(c));
  end));
end

local function isBlank(s)
  return type(s) ~= 'string' or string.match(s, '^%s*$') ~= nil;
end

local function queryString(params)
  local parts = {};
  for _, item in ipairs(params) do
    if not isBlank(item[2]) then
      parts[#parts + 1] = urlEncode(item[1]) .. '=' .. urlEncode(item[2]);
    end
  end
  if #parts == 0 then return ''; end
  return '?' .. table.concat(parts, '&');
end

local function readAllAndClose(response)
  local body = response.readAll();
  response.close();
  return body;
end

local function statusCode(response)
  if response.getResponseCode then
    return response.getResponseCode();
  end
  return nil;
end

local function createHttp(opts)
  opts = opts or {};
  local httpLib = opts.http or http;
  local textutilsLib = opts.textutils or textutils;

  local api = {
    base64encode = base64encode,
    trimTrailingSlash = trimTrailingSlash,
    urlEncode = urlEncode,
    queryString = queryString,
  };

  function api.basicAuth(username, password)
    return 'Basic ' .. base64encode(tostring(username or '') .. ':' .. tostring(password or ''));
  end

  function api.jsonHeaders(options)
    options = options or {};
    local headers = {
      ['Content-Type'] = 'application/json',
      ['Accept'] = 'application/json',
    };
    if options.password and options.password ~= '' then
      headers['Authorization'] = api.basicAuth(options.username, options.password);
    end
    return headers;
  end

  function api.call(method, request)
    local ok, response, httpErr, errorResponse = pcall(httpLib[method], request);
    if not ok then
      return nil, 'http ' .. method .. ' threw: ' .. tostring(response);
    end
    response = response or errorResponse;
    if not response then
      return nil, 'serveur injoignable: ' .. tostring(httpErr or 'unknown error');
    end
    local code = statusCode(response);
    local body = readAllAndClose(response);
    return body, code;
  end

  function api.getJson(cfg, path)
    return api.call('get', {
      url = cfg.url .. path,
      headers = api.jsonHeaders(cfg),
      timeout = cfg.timeoutSeconds,
    });
  end

  function api.postJson(cfg, path, payload)
    return api.call('post', {
      url = cfg.url .. path,
      body = textutilsLib.serializeJSON(payload),
      headers = api.jsonHeaders(cfg),
      timeout = cfg.timeoutSeconds,
    });
  end

  return api;
end

return createHttp;
