defmodule Towerops.Organizations.Organization do
  @moduledoc """
  Organization schema for multi-tenant workspace management.

  Organizations contain:
  - Members (users with roles)
  - Sites (physical locations)
  - Devices (network equipment)
  - Default SNMP configuration
  - Default agent token for polling
  """
  use Ecto.Schema

  import Ecto.Changeset

  alias Ecto.Association.NotLoaded
  alias Towerops.Agents.AgentToken
  alias Towerops.Encrypted.Binary
  alias Towerops.Organizations.Invitation
  alias Towerops.Organizations.Membership

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "organizations" do
    field :name, :string
    field :slug, :string
    field :subscription_plan, :string, default: "free"
    field :use_sites, :boolean, default: false

    # device)
    field :snmp_version, :string, default: "2c"
    field :snmp_community, :string
    field :snmp_port, :integer, default: 161
    field :snmp_transport, :string, default: "udp"

    # SNMPv3 credentials (organization-level defaults cascade to site → device)
    field :snmpv3_security_level, :string
    field :snmpv3_username, :string
    field :snmpv3_auth_protocol, :string, default: "SHA-256"
    field :snmpv3_auth_password, Binary
    field :snmpv3_priv_protocol, :string, default: "AES"
    field :snmpv3_priv_password, Binary

    # MikroTik API credentials (organization-level defaults cascade to site → device)
    field :mikrotik_username, :string
    field :mikrotik_password, Binary
    field :mikrotik_port, :integer, default: 8729
    field :mikrotik_ssh_port, :integer, default: 22
    field :mikrotik_use_ssl, :boolean, default: true
    field :mikrotik_enabled, :boolean, default: false

    belongs_to :default_agent_token, AgentToken

    has_many :memberships, Membership
    has_many :users, through: [:memberships, :user]
    has_many :invitations, Invitation

    timestamps(type: :utc_datetime)
  end

  @type t :: %__MODULE__{
          id: Ecto.UUID.t(),
          name: String.t(),
          slug: String.t(),
          subscription_plan: String.t(),
          use_sites: boolean(),
          snmp_version: String.t() | nil,
          snmp_community: String.t() | nil,
          snmp_port: integer(),
          snmp_transport: String.t(),
          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,
          default_agent_token_id: Ecto.UUID.t() | nil,
          default_agent_token: NotLoaded.t() | AgentToken.t() | nil,
          memberships: NotLoaded.t() | [Membership.t()],
          users: NotLoaded.t() | [Towerops.Accounts.User.t()],
          invitations: NotLoaded.t() | [Invitation.t()],
          inserted_at: DateTime.t(),
          updated_at: DateTime.t()
        }

  @doc false
  def changeset(organization, attrs) do
    organization
    |> cast(attrs, [
      :name,
      :subscription_plan,
      :use_sites,
      :default_agent_token_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])
    |> validate_length(:name, min: 2, max: 100)
    |> validate_inclusion(:subscription_plan, ["free"])
    |> 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()
    |> generate_slug()
    |> validate_required([:slug])
    |> unique_constraint(:slug)
    |> foreign_key_constraint(:default_agent_token_id)
  end

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

    if snmp_version == "3" do
      changeset
      |> validate_required([:snmpv3_username])
      |> 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 generate_slug(changeset) do
    # Only generate slug if not already set
    if get_field(changeset, :slug) do
      changeset
    else
      # Generate a random, URL-safe slug using base62 encoding
      # 8 characters gives us 62^8 = ~218 trillion possible combinations
      slug =
        6
        |> :crypto.strong_rand_bytes()
        |> Base.encode64(padding: false)
        |> String.replace(~r/[^a-zA-Z0-9]/, "")
        |> String.slice(0..7)
        |> String.downcase()

      put_change(changeset, :slug, slug)
    end
  end
end
