defmodule Towerops.Snmp.MacDiscovery do
  @moduledoc """
  Discovers MAC address forwarding table entries via SNMP.

  Uses BRIDGE-MIB dot1dTpFdbTable (1.3.6.1.2.1.17.4.3) and Q-BRIDGE-MIB
  dot1qTpFdbTable (1.3.6.1.2.1.17.7.1.2.2) to retrieve MAC address
  forwarding database entries from network switches.
  """

  alias Towerops.Snmp.Client

  require Logger

  @type mac_entry :: %{
          required(:mac_address) => String.t(),
          required(:port_index) => integer(),
          optional(:vlan_id) => integer(),
          optional(:entry_status) => String.t(),
          required(:last_seen_at) => DateTime.t()
        }

  # BRIDGE-MIB FDB table OIDs
  # dot1dTpFdbTable - basic FDB without VLAN awareness
  @dot1d_tp_fdb_address "1.3.6.1.2.1.17.4.3.1.1"
  @dot1d_tp_fdb_port "1.3.6.1.2.1.17.4.3.1.2"
  @dot1d_tp_fdb_status "1.3.6.1.2.1.17.4.3.1.3"

  # Q-BRIDGE-MIB FDB table OIDs (VLAN-aware)
  @dot1q_tp_fdb_port "1.3.6.1.2.1.17.7.1.2.2.1.2"
  @dot1q_tp_fdb_status "1.3.6.1.2.1.17.7.1.2.2.1.3"

  @doc """
  Discovers MAC address forwarding table entries for a device.
  Returns {:ok, mac_entries} where mac_entries is a list of mac_entry maps.
  """
  @spec discover_mac_table(Client.connection_opts()) :: {:ok, [mac_entry()]}
  def discover_mac_table(client_opts) do
    # Try Q-BRIDGE-MIB first (VLAN-aware), fall back to BRIDGE-MIB
    case discover_qbridge_fdb(client_opts) do
      {:ok, [_ | _] = entries} ->
        {:ok, entries}

      _ ->
        discover_bridge_fdb(client_opts)
    end
  end

  # Discover using Q-BRIDGE-MIB (VLAN-aware)
  defp discover_qbridge_fdb(client_opts) do
    port_map = fetch_qbridge_ports(client_opts)
    status_map = fetch_qbridge_status(client_opts)

    if map_size(port_map) == 0 do
      {:ok, []}
    else
      entries =
        port_map
        |> Enum.map(fn {key, port_index} ->
          {vlan_id, mac_address} = parse_qbridge_key(key)
          status = Map.get(status_map, key)

          %{
            mac_address: mac_address,
            port_index: port_index,
            vlan_id: vlan_id,
            entry_status: fdb_status_to_string(status),
            last_seen_at: DateTime.truncate(DateTime.utc_now(), :second)
          }
        end)
        |> Enum.reject(&is_nil(&1.mac_address))

      {:ok, entries}
    end
  end

  # Discover using BRIDGE-MIB (no VLAN awareness)
  defp discover_bridge_fdb(client_opts) do
    mac_map = fetch_bridge_macs(client_opts)
    port_map = fetch_bridge_ports(client_opts)
    status_map = fetch_bridge_status(client_opts)

    if map_size(mac_map) == 0 do
      {:ok, []}
    else
      entries =
        mac_map
        |> Enum.map(fn {key, _mac_binary} ->
          mac_address = parse_mac_from_key(key)
          port_index = Map.get(port_map, key)
          status = Map.get(status_map, key)

          %{
            mac_address: mac_address,
            port_index: port_index,
            vlan_id: nil,
            entry_status: fdb_status_to_string(status),
            last_seen_at: DateTime.truncate(DateTime.utc_now(), :second)
          }
        end)
        |> Enum.reject(&(is_nil(&1.mac_address) or is_nil(&1.port_index)))

      {:ok, entries}
    end
  end

  # Fetch helpers for Q-BRIDGE-MIB
  defp fetch_qbridge_ports(client_opts) do
    case Client.walk(client_opts, @dot1q_tp_fdb_port) do
      {:ok, entries} when map_size(entries) > 0 ->
        Map.new(entries, fn {oid, value} ->
          key = extract_index(oid, @dot1q_tp_fdb_port)
          {key, value}
        end)

      _ ->
        %{}
    end
  end

  defp fetch_qbridge_status(client_opts) do
    case Client.walk(client_opts, @dot1q_tp_fdb_status) do
      {:ok, entries} when map_size(entries) > 0 ->
        Map.new(entries, fn {oid, value} ->
          key = extract_index(oid, @dot1q_tp_fdb_status)
          {key, value}
        end)

      _ ->
        %{}
    end
  end

  # Fetch helpers for BRIDGE-MIB
  defp fetch_bridge_macs(client_opts) do
    case Client.walk(client_opts, @dot1d_tp_fdb_address) do
      {:ok, entries} when map_size(entries) > 0 ->
        Map.new(entries, fn {oid, value} ->
          key = extract_index(oid, @dot1d_tp_fdb_address)
          {key, value}
        end)

      _ ->
        %{}
    end
  end

  defp fetch_bridge_ports(client_opts) do
    case Client.walk(client_opts, @dot1d_tp_fdb_port) do
      {:ok, entries} when map_size(entries) > 0 ->
        Map.new(entries, fn {oid, value} ->
          key = extract_index(oid, @dot1d_tp_fdb_port)
          {key, value}
        end)

      _ ->
        %{}
    end
  end

  defp fetch_bridge_status(client_opts) do
    case Client.walk(client_opts, @dot1d_tp_fdb_status) do
      {:ok, entries} when map_size(entries) > 0 ->
        Map.new(entries, fn {oid, value} ->
          key = extract_index(oid, @dot1d_tp_fdb_status)
          {key, value}
        end)

      _ ->
        %{}
    end
  end

  # Extract the index portion from an OID
  defp extract_index(oid, base_oid) do
    base_len = String.length(base_oid) + 1
    String.slice(oid, base_len..-1//1)
  end

  # Parse Q-BRIDGE key format: vlan_id.mac_octet1.mac_octet2...mac_octet6
  defp parse_qbridge_key(key) do
    case String.split(key, ".") do
      [vlan_id | mac_parts] when length(mac_parts) == 6 ->
        mac_address = format_mac_from_octets(mac_parts)
        {String.to_integer(vlan_id), mac_address}

      _ ->
        {nil, nil}
    end
  end

  # Parse MAC from BRIDGE-MIB key format: mac_octet1.mac_octet2...mac_octet6
  defp parse_mac_from_key(key) do
    parts = String.split(key, ".")

    if length(parts) == 6 do
      format_mac_from_octets(parts)
    end
  end

  # Format MAC address from octet strings
  defp format_mac_from_octets(octets) when length(octets) == 6 do
    octets
    |> Enum.map(&String.to_integer/1)
    |> Enum.map_join(":", &String.pad_leading(Integer.to_string(&1, 16), 2, "0"))
    |> String.downcase()
  end

  defp format_mac_from_octets(_), do: nil

  # Convert FDB status integer to string
  # 1 = other, 2 = invalid, 3 = learned, 4 = self, 5 = mgmt
  defp fdb_status_to_string(1), do: "other"
  defp fdb_status_to_string(2), do: "invalid"
  defp fdb_status_to_string(3), do: "learned"
  defp fdb_status_to_string(4), do: "self"
  defp fdb_status_to_string(5), do: "mgmt"
  defp fdb_status_to_string(_), do: nil
end
