defmodule Towerops.Snmp.IpAddress do
  @moduledoc """
  SNMP IP address schema for tracking IP addresses assigned to interfaces.

  IP addresses are discovered via:
  - IP-MIB ipAddrTable (IPv4)
  - IPV6-MIB ipv6AddrTable (IPv6)

  Each interface can have multiple IP addresses (primary and secondary/alias).
  """
  use Ecto.Schema

  import Ecto.Changeset

  alias Ecto.Association.NotLoaded
  alias Towerops.Snmp.Interface

  @valid_ip_types ~w(ipv4 ipv6)

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "snmp_ip_addresses" do
    field :ip_address, :string
    field :subnet_mask, :string
    field :prefix_length, :integer
    field :ip_type, :string
    field :is_primary, :boolean, default: false
    field :last_checked_at, :utc_datetime
    field :metadata, :map, default: %{}

    belongs_to :snmp_interface, Interface

    timestamps(type: :utc_datetime)
  end

  @type t :: %__MODULE__{
          id: Ecto.UUID.t(),
          ip_address: String.t(),
          subnet_mask: String.t() | nil,
          prefix_length: integer() | nil,
          ip_type: String.t(),
          is_primary: boolean(),
          last_checked_at: DateTime.t() | nil,
          metadata: map(),
          snmp_interface_id: Ecto.UUID.t(),
          snmp_interface: NotLoaded.t() | Interface.t(),
          inserted_at: DateTime.t(),
          updated_at: DateTime.t()
        }

  @doc false
  def changeset(ip_address, attrs) do
    ip_address
    |> cast(attrs, [
      :snmp_interface_id,
      :ip_address,
      :subnet_mask,
      :prefix_length,
      :ip_type,
      :is_primary,
      :last_checked_at,
      :metadata
    ])
    |> validate_required([:snmp_interface_id, :ip_address, :ip_type])
    |> validate_inclusion(:ip_type, @valid_ip_types)
    |> validate_prefix_length()
    |> unique_constraint([:snmp_interface_id, :ip_address])
    |> foreign_key_constraint(:snmp_interface_id)
  end

  defp validate_prefix_length(changeset) do
    case get_field(changeset, :prefix_length) do
      nil ->
        changeset

      prefix ->
        ip_type = get_field(changeset, :ip_type)
        max_prefix = if ip_type == "ipv6", do: 128, else: 32

        if prefix >= 0 and prefix <= max_prefix do
          changeset
        else
          add_error(changeset, :prefix_length, "must be between 0 and #{max_prefix}")
        end
    end
  end
end
