defmodule Towerops.Devices.Device do
  @moduledoc """
  Device schema for network devices being monitored.

  Devices belong to a site and organization, and can have:
  - SNMP configuration (community strings, polling settings)
  - Associated SNMP device record (interfaces, sensors)
  - Agent assignments for remote polling
  - Monitoring checks and alerts
  """
  use Ecto.Schema

  import Ecto.Changeset

  alias Ecto.Association.NotLoaded
  alias Towerops.Agents.AgentAssignment
  alias Towerops.Sites.Site
  alias Towerops.Snmp.Device, as: SnmpDevice

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "devices" do
    field :name, :string
    field :ip_address, :string
    field :description, :string
    field :status, Ecto.Enum, values: [:up, :down, :unknown], default: :unknown
    field :last_checked_at, :utc_datetime
    field :last_status_change_at, :utc_datetime
    field :monitoring_enabled, :boolean, default: true
    field :check_interval_seconds, :integer, default: 300

    # SNMP fields
    field :snmp_enabled, :boolean, default: false
    field :snmp_version, :string, default: "2c"
    field :snmp_community, :string
    field :snmp_port, :integer, default: 161
    field :last_discovery_at, :utc_datetime
    field :last_snmp_poll_at, :utc_datetime

    belongs_to :site, Site

    has_one :snmp_device, SnmpDevice
    has_many :agent_assignments, AgentAssignment

    timestamps(type: :utc_datetime)
  end

  @type t :: %__MODULE__{
          id: Ecto.UUID.t(),
          name: String.t(),
          ip_address: String.t(),
          description: String.t() | nil,
          status: :up | :down | :unknown,
          last_checked_at: DateTime.t() | nil,
          last_status_change_at: DateTime.t() | nil,
          monitoring_enabled: boolean(),
          check_interval_seconds: integer(),
          snmp_enabled: boolean(),
          snmp_version: String.t(),
          snmp_community: String.t() | nil,
          snmp_port: integer(),
          last_discovery_at: DateTime.t() | nil,
          last_snmp_poll_at: DateTime.t() | nil,
          site_id: Ecto.UUID.t(),
          site: NotLoaded.t() | Site.t(),
          snmp_device: NotLoaded.t() | SnmpDevice.t() | nil,
          agent_assignments: NotLoaded.t() | [AgentAssignment.t()],
          inserted_at: DateTime.t(),
          updated_at: DateTime.t()
        }

  @doc false
  def changeset(device, attrs) do
    device
    |> cast(attrs, [
      :name,
      :ip_address,
      :description,
      :site_id,
      :monitoring_enabled,
      :check_interval_seconds,
      :snmp_enabled,
      :snmp_version,
      :snmp_community,
      :snmp_port
    ])
    |> validate_required([:ip_address, :site_id])
    |> validate_name()
    |> validate_length(:name, min: 2, max: 200)
    |> validate_length(:description, max: 1000)
    |> validate_ip_address()
    |> validate_number(:check_interval_seconds, greater_than: 0, less_than_or_equal_to: 3600)
    |> validate_snmp()
    |> foreign_key_constraint(:site_id)
  end

  defp validate_name(changeset) do
    snmp_enabled = get_field(changeset, :snmp_enabled, false)
    name = get_field(changeset, :name)

    # Name is required only when SNMP is not enabled (ICMP only mode)
    # When SNMP is enabled, name can be empty and will be populated from sysName during discovery
    if !snmp_enabled and (is_nil(name) or name == "") do
      add_error(changeset, :name, "can't be blank (required when SNMP is disabled)")
    else
      changeset
    end
  end

  defp validate_ip_address(changeset) do
    case get_change(changeset, :ip_address) do
      nil ->
        changeset

      ip_string ->
        case parse_ip(ip_string) do
          {:ok, _} ->
            changeset

          {:error, _} ->
            add_error(changeset, :ip_address, "must be a valid IPv4 or IPv6 address")
        end
    end
  end

  defp parse_ip(ip_string) do
    ip_string
    |> String.to_charlist()
    |> :inet.parse_address()
  end

  defp validate_snmp(changeset) do
    snmp_enabled = get_field(changeset, :snmp_enabled)

    if snmp_enabled do
      changeset
      |> validate_required([:snmp_version], message: "required when SNMP is enabled")
      |> validate_inclusion(:snmp_version, ["1", "2c"], message: "must be either '1' or '2c'")
      |> validate_number(:snmp_port, greater_than: 0, less_than: 65_536)
    else
      changeset
    end
  end
end
