defmodule Towerops.Sites.Site do
  @moduledoc """
  Site schema representing physical locations within an organization.

  Sites contain devices and can have site-level SNMP configuration defaults.
  """
  use Ecto.Schema

  import Ecto.Changeset

  alias Ecto.Association.NotLoaded
  alias Towerops.Agents.AgentToken
  alias Towerops.Devices.Device
  alias Towerops.Encrypted.Binary
  alias Towerops.Organizations.Organization
  alias Towerops.Sites.Site

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "sites" do
    field :name, :string
    field :description, :string
    field :location, :string
    field :display_order, :integer

    # SNMP configuration (overrides organization default)
    field :snmp_version, :string
    field :snmp_community, :string
    field :snmp_port, :integer
    field :snmp_transport, :string

    # SNMPv3 credentials (overrides organization default, all nullable)
    field :snmpv3_security_level, :string
    field :snmpv3_username, :string
    field :snmpv3_auth_protocol, :string
    field :snmpv3_auth_password, Binary
    field :snmpv3_priv_protocol, :string
    field :snmpv3_priv_password, Binary

    # MikroTik API credentials (overrides organization default)
    field :mikrotik_username, :string
    field :mikrotik_password, Binary
    field :mikrotik_port, :integer
    field :mikrotik_ssh_port, :integer
    field :mikrotik_use_ssl, :boolean
    field :mikrotik_enabled, :boolean

    belongs_to :organization, Organization
    belongs_to :agent_token, AgentToken
    belongs_to :parent_site, Site
    has_many :child_sites, Site, foreign_key: :parent_site_id
    has_many :device, Device

    timestamps(type: :utc_datetime)
  end

  @type t :: %__MODULE__{
          id: Ecto.UUID.t(),
          name: String.t(),
          description: String.t() | nil,
          location: String.t() | nil,
          display_order: integer() | nil,
          snmp_version: String.t() | nil,
          snmp_community: String.t() | nil,
          snmp_port: integer() | nil,
          snmp_transport: String.t() | nil,
          snmpv3_security_level: String.t() | nil,
          snmpv3_username: String.t() | nil,
          snmpv3_auth_protocol: String.t() | nil,
          snmpv3_auth_password: String.t() | nil,
          snmpv3_priv_protocol: String.t() | nil,
          snmpv3_priv_password: String.t() | nil,
          mikrotik_username: String.t() | nil,
          mikrotik_password: String.t() | nil,
          mikrotik_port: integer() | nil,
          mikrotik_ssh_port: integer() | nil,
          mikrotik_use_ssl: boolean() | nil,
          mikrotik_enabled: boolean() | nil,
          organization_id: Ecto.UUID.t(),
          organization: NotLoaded.t() | Organization.t(),
          agent_token_id: Ecto.UUID.t() | nil,
          agent_token: NotLoaded.t() | AgentToken.t() | nil,
          parent_site_id: Ecto.UUID.t() | nil,
          parent_site: NotLoaded.t() | t() | nil,
          child_sites: NotLoaded.t() | [t()],
          device: NotLoaded.t() | [Device.t()],
          inserted_at: DateTime.t(),
          updated_at: DateTime.t()
        }

  @doc false
  def changeset(site, attrs) do
    site
    |> cast(attrs, [
      :name,
      :description,
      :location,
      :display_order,
      :organization_id,
      :agent_token_id,
      :parent_site_id,
      :snmp_version,
      :snmp_community,
      :snmp_port,
      :snmp_transport,
      :snmpv3_security_level,
      :snmpv3_username,
      :snmpv3_auth_protocol,
      :snmpv3_auth_password,
      :snmpv3_priv_protocol,
      :snmpv3_priv_password,
      :mikrotik_username,
      :mikrotik_password,
      :mikrotik_port,
      :mikrotik_ssh_port,
      :mikrotik_use_ssl,
      :mikrotik_enabled
    ])
    |> validate_required([:name, :organization_id])
    |> validate_length(:name, min: 2, max: 200)
    |> validate_length(:description, max: 1000)
    |> validate_length(:location, max: 200)
    |> validate_inclusion(:snmp_version, ["1", "2c", "3"], message: "must be 1, 2c, or 3")
    |> validate_number(:snmp_port, greater_than: 0, less_than: 65_536)
    |> validate_snmpv3_fields()
    |> validate_mikrotik_fields()
    |> foreign_key_constraint(:organization_id)
    |> foreign_key_constraint(:agent_token_id)
    |> foreign_key_constraint(:parent_site_id)
    |> unique_constraint(:name,
      name: :sites_organization_id_name_index,
      message: "has already been taken for this organization"
    )
    |> validate_not_circular_parent()
  end

  defp validate_snmpv3_fields(changeset) do
    snmp_version = get_field(changeset, :snmp_version)

    if snmp_version == "3" do
      changeset
      |> derive_snmpv3_security_level()
      |> validate_snmpv3_auth_required()
      |> validate_snmpv3_priv_required()
      |> validate_password_length(:snmpv3_auth_password, 8)
      |> validate_password_length(:snmpv3_priv_password, 8)
    else
      changeset
    end
  end

  defp derive_snmpv3_security_level(changeset) do
    # Automatically derive security level based on what fields are present
    auth_protocol = get_field(changeset, :snmpv3_auth_protocol)
    priv_protocol = get_field(changeset, :snmpv3_priv_protocol)

    security_level =
      cond do
        auth_protocol in [nil, ""] -> "noAuthNoPriv"
        priv_protocol not in [nil, ""] -> "authPriv"
        true -> "authNoPriv"
      end

    put_change(changeset, :snmpv3_security_level, security_level)
  end

  defp validate_snmpv3_auth_required(changeset) do
    auth_protocol = get_field(changeset, :snmpv3_auth_protocol)

    if auth_protocol in [nil, ""] do
      changeset
    else
      changeset
      |> validate_required([:snmpv3_auth_password],
        message: "required when auth protocol is selected"
      )
      |> validate_inclusion(:snmpv3_auth_protocol, [
        "MD5",
        "SHA",
        "SHA-224",
        "SHA-256",
        "SHA-384",
        "SHA-512"
      ])
    end
  end

  defp validate_snmpv3_priv_required(changeset) do
    priv_protocol = get_field(changeset, :snmpv3_priv_protocol)

    if priv_protocol in [nil, ""] do
      changeset
    else
      changeset
      |> validate_required([:snmpv3_priv_password],
        message: "required when privacy protocol is selected"
      )
      |> validate_inclusion(:snmpv3_priv_protocol, ["DES", "AES", "AES-192", "AES-256", "AES-256-C"])
    end
  end

  defp validate_password_length(changeset, field, min_length) do
    case get_change(changeset, field) do
      nil ->
        changeset

      password when byte_size(password) < min_length ->
        add_error(changeset, field, "must be at least #{min_length} characters")

      _ ->
        changeset
    end
  end

  defp validate_mikrotik_fields(changeset) do
    changeset
    |> validate_number(:mikrotik_port, greater_than: 0, less_than: 65_536)
    |> validate_mikrotik_required_fields()
  end

  defp validate_mikrotik_required_fields(changeset) do
    if get_field(changeset, :mikrotik_enabled) do
      validate_required(changeset, [:mikrotik_username])
    else
      changeset
    end
  end

  defp validate_not_circular_parent(changeset) do
    parent_site_id = get_change(changeset, :parent_site_id)
    site_id = get_field(changeset, :id)

    if parent_site_id && site_id && parent_site_id == site_id do
      add_error(changeset, :parent_site_id, "cannot be the same as the site itself")
    else
      changeset
    end
  end
end
