defmodule Microwaveprop.Commercial.SnmpClient do
  @moduledoc false

  require Logger

  # UBNT-AirFIBER-MIB OIDs (base 1.3.6.1.4.1.41112.1.3)
  @af_base "1.3.6.1.4.1.41112.1.3"
  @af11x_oids %{
    "#{@af_base}.2.1.11.1" => :rx_power_0,
    "#{@af_base}.2.1.14.1" => :rx_power_1,
    "#{@af_base}.1.1.9.1" => :tx_power,
    "#{@af_base}.2.1.2.1" => :cur_tx_mod_rate,
    "#{@af_base}.2.1.18.1" => :remote_tx_mod_rate,
    "#{@af_base}.2.1.5.1" => :rx_capacity,
    "#{@af_base}.2.1.6.1" => :tx_capacity,
    "#{@af_base}.2.1.17.1" => :remote_tx_power,
    "#{@af_base}.2.1.19.1" => :remote_rx_power_0,
    "#{@af_base}.2.1.22.1" => :remote_rx_power_1,
    "#{@af_base}.2.1.26.1" => :link_state,
    "#{@af_base}.2.1.44.1" => :link_uptime,
    "#{@af_base}.2.1.8.1" => :radio_temp_0_c,
    "#{@af_base}.2.1.10.1" => :radio_temp_1_c,
    "#{@af_base}.1.1.5.1" => :tx_freq_mhz,
    "#{@af_base}.1.1.6.1" => :rx_freq_mhz,
    "#{@af_base}.2.1.4.1" => :link_distance_m
  }

  # AF60-LR static OIDs
  @af60_base "1.3.6.1.4.1.41112.1.11.1"
  @af60_static_oids %{
    "#{@af60_base}.2.5.1" => :radio_temp_0_c,
    "#{@af60_base}.2.6.1" => :link_state,
    "#{@af60_base}.1.2.1" => :tx_freq_mhz
  }

  # AF60 station table column -> field name + unit conversion
  @af60_station_cols %{
    3 => {:rx_power_0, :raw},
    4 => {:tx_power, :raw},
    5 => {:cur_tx_mod_rate, :raw},
    6 => {:remote_tx_mod_rate, :raw},
    7 => {:tx_capacity, :kbps_to_mbps},
    8 => {:rx_capacity, :kbps_to_mbps},
    15 => {:link_distance_m, :raw},
    17 => {:link_uptime, :centiseconds_to_seconds},
    18 => {:remote_rx_power_0, :raw},
    19 => {:remote_tx_power, :raw}
  }

  def poll(host, community, "af11x"), do: poll_af11x(host, community)
  def poll(host, community, "af60"), do: poll_af60(host, community)

  def poll_af11x(host, community) do
    oids = Map.keys(@af11x_oids)
    args = ["-v1", "-c", community, "-t", "5", "-r", "1", host | oids]

    case run_cmd("snmpget", args) do
      {output, 0} ->
        result = parse_snmpget_output(output, :af11x)
        if map_size(result) > 0, do: {:ok, result}, else: {:error, :empty_response}

      {output, _exit_code} ->
        Logger.warning("snmpget failed for #{host}: #{String.trim(output)}")
        {:error, :snmp_failed}
    end
  end

  def poll_af60(host, community) do
    static_oids = Map.keys(@af60_static_oids)
    static_args = ["-v1", "-c", community, "-t", "5", "-r", "1", host | static_oids]

    station_oids = Enum.map(Map.keys(@af60_station_cols), &"#{@af60_base}.3.1.#{&1}")
    station_args = ["-v1", "-c", community, "-t", "5", "-r", "1", host | station_oids]

    with {static_out, 0} <- run_cmd("snmpget", static_args),
         {station_out, 0} <- run_cmd("snmpgetnext", station_args) do
      result = parse_af60_output(static_out, station_out)
      if map_size(result) > 0, do: {:ok, result}, else: {:error, :empty_response}
    else
      {output, _} ->
        Logger.warning("snmp failed for AF60 #{host}: #{String.trim(output)}")
        {:error, :snmp_failed}
    end
  end

  def parse_snmpget_output(output, :af11x) do
    output
    |> parse_lines()
    |> Enum.reduce(%{}, fn {oid, value}, acc ->
      case find_af11x_field(oid) do
        nil -> acc
        field -> Map.put(acc, field, value)
      end
    end)
  end

  def parse_af60_output(static_output, station_output) do
    static =
      static_output
      |> parse_lines()
      |> Enum.reduce(%{}, fn {oid, value}, acc ->
        case find_af60_static_field(oid) do
          nil -> acc
          field -> Map.put(acc, field, value)
        end
      end)

    station =
      station_output
      |> parse_lines()
      |> Enum.reduce(%{}, fn {oid, value}, acc ->
        case find_af60_station_field(oid) do
          nil -> acc
          {field, conversion} -> Map.put(acc, field, convert_value(value, conversion))
        end
      end)

    Map.merge(static, station)
  end

  # --- Private ---

  defp parse_lines(output) do
    output
    |> String.split("\n", trim: true)
    |> Enum.flat_map(fn line ->
      case parse_snmp_line(line) do
        {_oid, _value} = pair -> [pair]
        nil -> []
      end
    end)
  end

  defp run_cmd(cmd, args) do
    System.cmd(cmd, args, stderr_to_stdout: true)
  rescue
    ErlangError -> {"command not found: #{cmd}", 127}
  end

  defp parse_snmp_line(line) do
    line = String.trim(line)

    with [oid_part, value_part] <- String.split(line, " = ", parts: 2),
         false <- String.contains?(value_part, "No Such"),
         oid = oid_part |> String.trim() |> normalize_oid(),
         value when not is_nil(value) <- parse_snmp_value(value_part) do
      {oid, value}
    else
      _ -> nil
    end
  end

  defp normalize_oid(oid) do
    oid
    |> String.replace(~r/^SNMPv2-SMI::enterprises\./, "1.3.6.1.4.1.")
    |> String.replace(~r/^iso\./, "1.")
    |> String.replace(~r/^\./, "")
  end

  defp parse_snmp_value(value_str) do
    case Regex.run(~r/:\s*(.+)$/, String.trim(value_str)) do
      [_, raw] ->
        raw = String.trim(raw, "\"")

        case Integer.parse(raw) do
          {val, _} -> val
          :error -> nil
        end

      nil ->
        nil
    end
  end

  defp find_af11x_field(oid) do
    Enum.find_value(@af11x_oids, fn {known_oid, field} ->
      if oid == known_oid, do: field
    end)
  end

  defp find_af60_static_field(oid) do
    Enum.find_value(@af60_static_oids, fn {known_oid, field} ->
      if oid == known_oid, do: field
    end)
  end

  defp find_af60_station_field(oid) do
    Enum.find_value(@af60_station_cols, fn {col, field_and_conv} ->
      prefix = "#{@af60_base}.3.1.#{col}."
      if String.starts_with?(oid, prefix), do: field_and_conv
    end)
  end

  defp convert_value(value, :raw), do: value
  defp convert_value(value, :kbps_to_mbps), do: div(value, 1000)
  defp convert_value(value, :centiseconds_to_seconds), do: div(value, 100)
end
