--
-- json.lua — minimal JSON encode/decode for Lua 5.1 (UTF-8 byte strings).
-- Adapted from rxi/json.lua (MIT). Used by the browser-automation agent to talk
-- to the DeepSeek chat-completions API. `require "json"` returns this table.
--

local json = {}

------------------------------------------------------------------- encode
local encode

local escape_char_map = {
  ["\\"] = "\\\\", ["\""] = "\\\"", ["\b"] = "\\b", ["\f"] = "\\f",
  ["\n"] = "\\n",  ["\r"] = "\\r",  ["\t"] = "\\t",
}

local function escape_char(c)
  return escape_char_map[c] or string.format("\\u%04x", c:byte())
end

local function encode_nil() return "null" end

local function encode_string(s)
  return '"' .. s:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end

local function encode_number(n)
  if n ~= n or n <= -math.huge or n >= math.huge then
    error("unexpected number value '" .. tostring(n) .. "'")
  end
  return string.format("%.14g", n)
end

local function encode_table(t)
  local res = {}
  local n = 0
  for k in pairs(t) do
    if type(k) ~= "number" then n = nil; break end
    n = n + 1
  end
  if n ~= nil and n == #t then          -- array (incl. empty)
    if #t == 0 then return "[]" end
    for i = 1, #t do res[i] = encode(t[i]) end
    return "[" .. table.concat(res, ",") .. "]"
  else                                  -- object
    local i = 0
    for k, v in pairs(t) do
      i = i + 1
      res[i] = encode_string(tostring(k)) .. ":" .. encode(v)
    end
    return "{" .. table.concat(res, ",") .. "}"
  end
end

local type_func_map = {
  ["nil"] = encode_nil, ["table"] = encode_table, ["string"] = encode_string,
  ["number"] = encode_number, ["boolean"] = tostring,
}

encode = function(v)
  local f = type_func_map[type(v)]
  if f then return f(v) end
  error("unexpected type '" .. type(v) .. "'")
end

function json.encode(v) return encode(v) end

------------------------------------------------------------------- decode
local parse

local function create_set(...)
  local res = {}
  local args = { ... }
  for i = 1, #args do res[args[i]] = true end
  return res
end

local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = { ["true"] = true, ["false"] = false, ["null"] = nil }

local function next_char(str, idx, set, negate)
  for i = idx, #str do
    if set[str:sub(i, i)] ~= negate then return i end
  end
  return #str + 1
end

local function decode_error(str, idx, msg)
  error(string.format("json decode error at %d: %s", idx, msg))
end

local function codepoint_to_utf8(n)
  local f = math.floor
  if n <= 0x7f then return string.char(n)
  elseif n <= 0x7ff then return string.char(f(n / 64) + 192, n % 64 + 128)
  elseif n <= 0xffff then return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
  elseif n <= 0x10ffff then return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, f(n % 4096 / 64) + 128, n % 64 + 128) end
  error(string.format("invalid unicode codepoint '%x'", n))
end

local function parse_unicode_escape(s)
  local n1 = tonumber(s:sub(1, 4), 16)
  local n2 = tonumber(s:sub(7, 10), 16)
  if n2 then
    return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
  else
    return codepoint_to_utf8(n1)
  end
end

-- Lua strings are immutable, so the original `res = res .. piece` pattern
-- allocated a brand-new string on every concat. For an LLM response that's
-- 8-32 KB of mostly-no-escape JSON string this was O(n^2). Accumulate into
-- a table and table.concat once at the end - O(n).
local _esc_lut = { ['"']='"', ['\\']='\\', ['/']='/', b='\b', f='\f', n='\n', r='\r', t='\t' }
local function parse_string(str, i)
  local parts = {}
  local j = i + 1
  local k = j
  while j <= #str do
    local x = str:byte(j)
    if x < 32 then
      decode_error(str, j, "control character in string")
    elseif x == 92 then -- backslash
      parts[#parts + 1] = str:sub(k, j - 1)
      j = j + 1
      local c = str:sub(j, j)
      if c == "u" then
        local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
                 or str:match("^%x%x%x%x", j + 1)
                 or decode_error(str, j - 1, "invalid unicode escape in string")
        parts[#parts + 1] = parse_unicode_escape(hex)
        j = j + #hex
      else
        if not _esc_lut[c] then
          decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
        end
        parts[#parts + 1] = _esc_lut[c]
      end
      k = j + 1
    elseif x == 34 then -- quote
      parts[#parts + 1] = str:sub(k, j - 1)
      return table.concat(parts), j + 1
    end
    j = j + 1
  end
  decode_error(str, i, "expected closing quote for string")
end

local function parse_number(str, i)
  local x = next_char(str, i, delim_chars)
  local s = str:sub(i, x - 1)
  local n = tonumber(s)
  if not n then decode_error(str, i, "invalid number '" .. s .. "'") end
  return n, x
end

local function parse_literal(str, i)
  local x = next_char(str, i, delim_chars)
  local word = str:sub(i, x - 1)
  if not literals[word] then decode_error(str, i, "invalid literal '" .. word .. "'") end
  return literal_map[word], x
end

local function parse_array(str, i)
  local res = {}
  local n = 1
  i = i + 1
  while 1 do
    local x
    i = next_char(str, i, space_chars, true)
    if str:sub(i, i) == "]" then i = i + 1; break end
    x, i = parse(str, i)
    res[n] = x; n = n + 1
    i = next_char(str, i, space_chars, true)
    local chr = str:sub(i, i); i = i + 1
    if chr == "]" then break end
    if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
  end
  return res, i
end

local function parse_object(str, i)
  local res = {}
  i = i + 1
  while 1 do
    local key, val
    i = next_char(str, i, space_chars, true)
    if str:sub(i, i) == "}" then i = i + 1; break end
    if str:sub(i, i) ~= '"' then decode_error(str, i, "expected string for key") end
    key, i = parse(str, i)
    i = next_char(str, i, space_chars, true)
    if str:sub(i, i) ~= ":" then decode_error(str, i, "expected ':' after key") end
    i = next_char(str, i + 1, space_chars, true)
    val, i = parse(str, i)
    res[key] = val
    i = next_char(str, i, space_chars, true)
    local chr = str:sub(i, i); i = i + 1
    if chr == "}" then break end
    if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
  end
  return res, i
end

local char_func_map = {
  ['"'] = parse_string, ["0"] = parse_number, ["1"] = parse_number, ["2"] = parse_number,
  ["3"] = parse_number, ["4"] = parse_number, ["5"] = parse_number, ["6"] = parse_number,
  ["7"] = parse_number, ["8"] = parse_number, ["9"] = parse_number, ["-"] = parse_number,
  ["t"] = parse_literal, ["f"] = parse_literal, ["n"] = parse_literal,
  ["["] = parse_array, ["{"] = parse_object,
}

parse = function(str, idx)
  local chr = str:sub(idx, idx)
  local f = char_func_map[chr]
  if f then return f(str, idx) end
  decode_error(str, idx, "unexpected character '" .. chr .. "'")
end

function json.decode(str)
  if type(str) ~= "string" then error("expected string, got " .. type(str)) end
  local res, idx = parse(str, next_char(str, 1, space_chars, true))
  idx = next_char(str, idx, space_chars, true)
  if idx <= #str then decode_error(str, idx, "trailing garbage") end
  return res
end

return json
