local ubus = require("ubus")
local bit32 = require("bit32")

local function get_wifi_interfaces()
  local conn = ubus.connect()
  local ubuslist = conn:objects()
  local interfaces = {}

  for _, net in ipairs(ubuslist) do
    if net:find("^hostapd%.") then
      local ifname = net:gsub("hostapd%.", "")
      table.insert(interfaces, ifname)
    end
  end
  conn:close()
  return interfaces
end

local function is_ubus_interface(ubus_interfaces, interface)
  for i = 1, #ubus_interfaces do
    if ubus_interfaces[i] == interface then
      return true
    end
  end
  return false
end

local function get_wifi_interface_labels()
  local u = ubus.connect()
  local status = u:call("network.wireless", "status", {})
  local interfaces = {}
  local ubus_interfaces = get_wifi_interfaces()

  for _, dev_table in pairs(status) do
    for _, intf in ipairs(dev_table["interfaces"]) do
      if is_ubus_interface(ubus_interfaces, intf["ifname"]) then
        -- Migrate this to ubus interface once it exposes all interesting labels
        local handle = io.popen("hostapd_cli -i " .. intf["ifname"] .. " status")
        local hostapd_status = handle:read("*a")
        handle:close()

        local hostapd = {}
        local bss_idx = -1
        for line in hostapd_status:gmatch("[^\r\n]+") do
          local name, value = string.match(line, "(.+)=(.+)")
          if name == "freq" then
            hostapd["freq"] = value
          elseif name == "channel" then
            hostapd["channel"] = value
          -- hostapd gives us all bss on the relevant phy, find the one we're interested in
          elseif string.match(name, "bss%[%d%]") then
            if value == intf["ifname"] then
              bss_idx = tonumber(string.match(name, "bss%[(%d)%]"))
            end
          elseif bss_idx >= 0 then
            if name == "bssid[" .. bss_idx .. "]" then
              hostapd["bssid"] = value
            elseif name == "ssid[" .. bss_idx .. "]" then
              hostapd["ssid"] = value
            end
          end
        end

        local labels = {
          vif = intf["ifname"],
          ssid = hostapd["ssid"],
          bssid = hostapd["bssid"],
          encryption = intf["config"]["encryption"], -- In a mixed scenario it would be good to know if A or B was used
          frequency = hostapd["freq"],
          channel = hostapd["channel"],
        }

        table.insert(interfaces, labels)
      end
    end
  end

  return interfaces
end

local function scrape()
  local metric_hostapd_station_rx_packets = metric("hostapd_station_receive_packets_total", "counter")
  local metric_hostapd_station_rx_bytes = metric("hostapd_station_receive_bytes_total", "counter")
  local metric_hostapd_station_tx_packets = metric("hostapd_station_transmit_packets_total", "counter")
  local metric_hostapd_station_tx_bytes = metric("hostapd_station_transmit_bytes_total", "counter")

  local metric_hostapd_station_signal = metric("hostapd_station_signal_dbm", "gauge")
  local metric_hostapd_station_connected_time = metric("hostapd_station_connected_seconds_total", "counter")
  local metric_hostapd_station_inactive_msec = metric("hostapd_station_inactive_seconds", "gauge")

  local metric_hostapd_station_flag_auth = metric("hostapd_station_flag_auth", "gauge")
  local metric_hostapd_station_flag_assoc = metric("hostapd_station_flag_assoc", "gauge")

  local metric_hostapd_station_flag_short_preamble = metric("hostapd_station_flag_short_preamble", "gauge")

  local metric_hostapd_station_flag_ht = metric("hostapd_station_flag_ht", "gauge")
  local metric_hostapd_station_flag_vht = metric("hostapd_station_flag_vht", "gauge")
  local metric_hostapd_station_flag_he = metric("hostapd_station_flag_he", "gauge")

  local metric_hostapd_station_flag_mfp = metric("hostapd_station_flag_mfp", "gauge")
  local metric_hostapd_station_flag_wmm = metric("hostapd_station_flag_wmm", "gauge")

  local metric_hostapd_station_sae_group = metric("hostapd_station_sae_group", "gauge")

  local metric_hostapd_station_vht_capb_su_beamformee = metric("hostapd_station_vht_capb_su_beamformee", "gauge")
  local metric_hostapd_station_vht_capb_mu_beamformee = metric("hostapd_station_vht_capb_mu_beamformee", "gauge")

  local function evaluate_metrics(labels, kv)
    values = {}
    for k, v in pairs(kv) do
      values[k] = v
    end

    -- check if values exist, they may not due to race conditions while querying
    if values["flags"] then
      local flags = {}
      for flag in string.gmatch(values["flags"], "%u+") do
        flags[flag] = true
      end

      metric_hostapd_station_flag_auth(labels, flags["AUTH"] and 1 or 0)
      metric_hostapd_station_flag_assoc(labels, flags["ASSOC"] and 1 or 0)

      metric_hostapd_station_flag_short_preamble(labels, flags["SHORT_PREAMBLE"] and 1 or 0)

      metric_hostapd_station_flag_ht(labels, flags["HT"] and 1 or 0)
      metric_hostapd_station_flag_vht(labels, flags["VHT"] and 1 or 0)
      metric_hostapd_station_flag_he(labels, flags["HE"] and 1 or 0)

      metric_hostapd_station_flag_wmm(labels, flags["WMM"] and 1 or 0)
      metric_hostapd_station_flag_mfp(labels, flags["MFP"] and 1 or 0)
    end

    -- these metrics can reasonably default to zero, when missing
    metric_hostapd_station_rx_packets(labels, values["rx_packets"] or 0)
    metric_hostapd_station_rx_bytes(labels, values["rx_bytes"] or 0)
    metric_hostapd_station_tx_packets(labels, values["tx_packets"] or 0)
    metric_hostapd_station_tx_bytes(labels, values["tx_bytes"] or 0)

    -- and these metrics can't be defaulted, so check again
    if values["inactive_msec"] ~= nil then
      metric_hostapd_station_inactive_msec(labels, values["inactive_msec"] / 1000)
    end

    if values["signal"] ~= nil then
      metric_hostapd_station_signal(labels, values["signal"])
    end

    if values["connected_time"] ~= nil then
      metric_hostapd_station_connected_time(labels, values["connected_time"])
    end

    if values["vht_caps_info"] ~= nil then
      local caps = tonumber(string.gsub(values["vht_caps_info"], "0x", ""), 16)
      metric_hostapd_station_vht_capb_su_beamformee(labels, bit32.band(bit32.lshift(1, 12), caps) > 0 and 1 or 0)
      metric_hostapd_station_vht_capb_mu_beamformee(labels, bit32.band(bit32.lshift(1, 20), caps) > 0 and 1 or 0)
    else
      metric_hostapd_station_vht_capb_su_beamformee(labels, 0)
      metric_hostapd_station_vht_capb_mu_beamformee(labels, 0)
    end

    if values["sae_group"] ~= nil then
      metric_hostapd_station_sae_group(labels, values["sae_group"])
    end
  end

  for _, labels in ipairs(get_wifi_interface_labels()) do
    local vif = labels["vif"]
    local handle = io.popen("hostapd_cli -i " .. vif .. " all_sta")
    local all_sta = handle:read("*a")
    handle:close()

    local station = nil
    local metrics = {}

    for line in all_sta:gmatch("[^\r\n]+") do
      if string.match(line, "^%x[0123456789aAbBcCdDeE]:%x%x:%x%x:%x%x:%x%x:%x%x$") then
        -- the first time we see a mac we have no previous station to eval, so don't
        if station ~= nil then
          labels.station = station
          evaluate_metrics(labels, metrics)
        end

        -- remember current station bssid and truncate metrics
        station = line
        metrics = {}
      else
        local key, value = string.match(line, "(.+)=(.+)")
        if key ~= nil then
          metrics[key] = value
        end
      end
    end

    -- the final station, check if there ever was one, will need a metrics eval as well
    if station ~= nil then
      labels.station = station
      evaluate_metrics(labels, metrics)
    end
  end
end

return { scrape = scrape }
