# SPDX-FileCopyrightText: 2023 Frank Hunleth
#
# SPDX-License-Identifier: Apache-2.0
#
defmodule Toolshed.Core.MulticastAddresses do
  defmodule Impl do
    @moduledoc false

    @doc false
    @spec read_or_empty(Path.t()) :: String.t()
    def read_or_empty(path) do
      case File.read(path) do
        {:ok, contents} -> contents
        _other -> ""
      end
    end

    @doc false
    @spec process_proc(String.t(), String.t(), String.t()) :: String.t()
    def process_proc(dev_mcast, igmp, igmp6) do
      all =
        process_link(dev_mcast) ++
          process_igmp(igmp) ++
          process_igmp6(igmp6)

      if_map =
        Enum.group_by(all, fn {_type, index, ifname, _address} -> {index, ifname} end, fn {type,
                                                                                           _index,
                                                                                           _ifname,
                                                                                           address} ->
          {type, address}
        end)

      if_keys = Map.keys(if_map) |> Enum.sort()

      Enum.map(if_keys, fn if_key -> format(if_key, if_map[if_key]) end)
      |> IO.chardata_to_string()
    end

    defp format({index, ifname}, addresses) do
      header = "#{index}: #{ifname}\n"
      lines = Enum.map(addresses, fn {type, address} -> "   #{type} #{address}\n" end)
      [header, lines]
    end

    defp process_link(dev_mcast) do
      dev_mcast
      |> String.split("\n")
      |> Enum.map(&process_one_link/1)
      |> Enum.filter(fn x -> x end)
    end

    defp process_one_link(line) do
      case String.split(line, " ", trim: true) do
        [index, ifname, _users, _ignore, mac_address] ->
          {:link, String.to_integer(index), ifname, to_pretty_mac(mac_address)}

        _ ->
          nil
      end
    end

    defp to_pretty_mac(<<a::2-bytes, b::2-bytes, c::2-bytes, d::2-bytes, e::2-bytes, f::2-bytes>>) do
      <<a::binary, ?:, b::binary, ?:, c::binary, ?:, d::binary, ?:, e::binary, ?:, f::binary>>
    end

    defp process_igmp(igmp) do
      all_lines = String.split(igmp, "\n")

      # Skip the header
      lines = tl(all_lines)

      parse_igmp_lines(0, :unknown, lines, [])
    end

    defp parse_igmp_lines(_index, _ifname, [], result), do: result

    defp parse_igmp_lines(index, ifname, [<<lead_char, info::binary>> | rest], result)
         when lead_char in [?\s, ?\t] do
      case String.split(info, [" ", "\t"], trim: true) do
        [address | _rest] ->
          record = {:inet, index, ifname, to_pretty_ipv4(address)}
          parse_igmp_lines(index, ifname, rest, [record | result])

        _other ->
          parse_igmp_lines(index, ifname, rest, result)
      end
    end

    defp parse_igmp_lines(index, ifname, [if_line | rest], result) do
      case String.split(if_line, [" ", "\t"], trim: true) do
        [new_index, new_ifname | _rest] ->
          parse_igmp_lines(String.to_integer(new_index), new_ifname, rest, result)

        _ ->
          parse_igmp_lines(index, ifname, rest, result)
      end
    end

    defp process_igmp6(igmp6) do
      igmp6
      |> String.split("\n")
      |> Enum.map(&process_one_igmp6/1)
      |> Enum.filter(fn x -> x end)
    end

    defp process_one_igmp6(line) do
      case String.split(line, " ", trim: true) do
        [index, ifname, address, _users, _offset, _ignore] ->
          {:inet6, String.to_integer(index), ifname, to_pretty_ipv6(address)}

        _ ->
          nil
      end
    end

    defp to_pretty_ipv4(<<a::2-bytes, b::2-bytes, c::2-bytes, d::2-bytes>>) do
      {
        String.to_integer(d, 16),
        String.to_integer(c, 16),
        String.to_integer(b, 16),
        String.to_integer(a, 16)
      }
      |> :inet.ntoa()
      |> to_string()
    end

    defp to_pretty_ipv6(
           <<a::4-bytes, b::4-bytes, c::4-bytes, d::4-bytes, e::4-bytes, f::4-bytes, g::4-bytes,
             h::4-bytes>>
         ) do
      {
        String.to_integer(a, 16),
        String.to_integer(b, 16),
        String.to_integer(c, 16),
        String.to_integer(d, 16),
        String.to_integer(e, 16),
        String.to_integer(f, 16),
        String.to_integer(g, 16),
        String.to_integer(h, 16)
      }
      |> :inet.ntoa()
      |> to_string()
    end
  end

  @doc """
  List all active multicast addresses

  This lists out multicast addresses by network interface
  similar to `ip maddr show`. It currently only works on
  Linux.
  """
  @spec multicast_addresses() :: :ok
  def multicast_addresses() do
    dev_mcast = Impl.read_or_empty("/proc/net/dev_mcast")
    igmp = Impl.read_or_empty("/proc/net/igmp")
    igmp6 = Impl.read_or_empty("/proc/net/igmp6")

    Impl.process_proc(dev_mcast, igmp, igmp6)
    |> IO.puts()
  end
end
