local function tofile(fname, text)
  local f = io.open(fname, 'w')
  if not f then
    error(('failed to write: %s'):format(f))
  else
    f:write(text)
    f:close()
  end
end

local function read_json(file)
  local uri = 'https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/master/json/'
    .. file

  local res = vim.system({ 'curl', '--no-progress-meter', uri, '-o', '-' }):wait()
  if res.code ~= 0 then
    vim.print(res)
    error(res.stdout)
  end
  return vim.json.decode(res.stdout)
end

local function escape(str)
  return string.gsub(str, '\n', '<br>')
end

local function format_ref(domain_name, ref)
  local tokens = vim.split(ref, '.', { plain = true })
  -- $ref contain the domain if the type was defined in another domain
  if #tokens > 1 then
    return 'CDP.' .. ref
  else
    return ('CDP.%s.%s'):format(domain_name, ref)
  end
end

local function map_type(domain_name, t)
  if t.type == 'array' then
    return ('%s[]'):format(map_type(domain_name, t.items))
  elseif t.type == 'object' then
    return 'table'
  elseif t.type == 'string' and t.enum then
    for i in ipairs(t.enum) do
      t.enum[i] = ('"%s"'):format(t.enum[i])
    end
    return table.concat(t.enum, ' | ')
  elseif t['$ref'] then
    return format_ref(domain_name, t['$ref'])
  else
    return t.type
  end
end

local function gen_meta(descriptors)
  local output = {
    '-- Generated by gen_protocol.lua',
    '',
    '---@meta',
    "error('Cannot require a meta file')",
    '',
    '---@class CDP._ClientContainer',
    '---@field client CDP.Client',
    '',
  }

  local function multiline_comment(comment)
    if not comment then
      return
    end
    for _, docstring in ipairs(vim.split(comment, '\n', { trimempty = true })) do
      -- escape any special comment
      if vim.startswith(docstring, '|') then
        docstring = '`' .. docstring
      end
      output[#output + 1] = '---' .. docstring
    end
  end

  local function gen_class(args)
    multiline_comment(args.description)

    if args.deprecated then
      output[#output + 1] = '---@deprecated'
    end
    output[#output + 1] = ('---@class CDP.%s'):format(args.name)

    if not args.params then
      return
    end

    for _, param in ipairs(args.params) do
      local fieldname = param.name
      if param.optional then
        fieldname = fieldname .. '?'
      end
      output[#output + 1] = vim.trim(
        ('---@field %s %s %s'):format(
          fieldname,
          map_type(args.domain_name, param),
          escape(param.description or '')
        )
      )
    end
  end

  local function gen_types(domain, domain_types)
    for _, domain_type in ipairs(domain_types) do
      if domain_type.type ~= 'object' or not domain_type.properties then
        output[#output + 1] = vim.trim(
          ('---@alias CDP.%s.%s %s %s'):format(
            domain.domain,
            domain_type.id,
            map_type(domain.domain, domain_type),
            escape(domain_type.description or '')
          )
        )
      else
        gen_class({
          name = ('%s.%s'):format(domain.domain, domain_type.id),
          domain_name = domain.domain,
          description = domain_type.description,
          deprecated = domain_type.deprecated,
          params = domain_type.properties,
        })
      end
    end
  end

  for _, descriptor in ipairs(descriptors) do
    for _, domain in ipairs(descriptor.domains) do
      output[#output + 1] = ''

      -- First gen all types, and all parameter and return records
      if domain.types then
        gen_types(domain, domain.types)
      end

      if domain.commands then
        for _, cmd in ipairs(domain.commands) do
          if cmd.parameters then
            gen_class({
              name = ('%s.%s'):format(domain.domain, cmd.name .. '_parameters'),
              domain_name = domain.domain,
              params = cmd.parameters,
            })
          end
          if cmd.returns then
            gen_class({
              name = ('%s.%s'):format(domain.domain, cmd.name .. '_return'),
              domain_name = domain.domain,
              params = cmd.returns,
            })
          end
        end
      end

      if domain.events then
        for _, ev in ipairs(domain.events) do
          if ev.parameters then
            gen_class({
              name = ('%s.%s'):format(domain.domain, ev.name .. '_event'),
              domain_name = domain.domain,
              params = ev.parameters,
            })
          end
        end
      end

      -- Now gen all domain commands as fields of the domain
      gen_class({
        name = domain.domain .. ': CDP._ClientContainer',
        deprecated = domain.deprecated,
        description = domain.description,
      })

      if domain.commands then
        for _, cmd in ipairs(domain.commands) do
          local fullcmd_name = ('%s.%s'):format(domain.domain, cmd.name)
          multiline_comment(cmd.description)

          local params = ''
          if cmd.parameters then
            params = (', params: CDP.%s_parameters'):format(fullcmd_name)
          end
          local _return = ''
          if cmd.returns then
            _return = (', CDP.%s_return?'):format(fullcmd_name)
          end
          output[#output + 1] = ('---@field %s fun(self: CDP._ClientContainer%s): CDP.Error?%s'):format(
            cmd.name,
            params,
            _return
          )
        end
      end

      if domain.events then
        for _, ev in ipairs(domain.events) do
          local fullev_name = ('%s.%s'):format(domain.domain, ev.name)
          multiline_comment(ev.description)

          local params = ''
          if ev.parameters then
            params = ('event: CDP.%s_event'):format(fullev_name)
          end
          output[#output + 1] = ('---@field %s fun(self: CDP._ClientContainer, cb: fun(%s)): CDP.Error?'):format(
            ev.name,
            params
          )
        end
      end
    end
  end

  output[#output + 1] = '---@class CDP'
  for _, descriptor in ipairs(descriptors) do
    for _, domain in ipairs(descriptor.domains) do
      output[#output + 1] = ('---@field %s CDP.%s'):format(domain.domain, domain.domain)
    end
  end

  tofile('lua/chrome-remote/_meta/protocol.lua', table.concat(output, '\n'))
end

local descriptors = { read_json('js_protocol.json'), read_json('browser_protocol.json') }
gen_meta(descriptors)
