defmodule Towerops.Agents.AgentToken do
  @moduledoc """
  Schema for agent authentication tokens.

  Agent tokens are used to authenticate remote SNMP polling agents that run on customer networks.
  Tokens are generated with cryptographically secure random bytes (48 bytes = 384 bits) and
  stored in plaintext in the database. Tokens can be retrieved and viewed by authorized users.
  """
  use Ecto.Schema

  import Ecto.Changeset

  alias Towerops.Organizations.Organization

  # 48 bytes = 384 bits of cryptographically secure randomness
  @rand_size 48

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "agent_tokens" do
    field :token, :string
    field :name, :string
    field :last_seen_at, :utc_datetime
    field :last_ip, :string
    field :enabled, :boolean, default: true
    field :is_cloud_poller, :boolean, default: false
    field :allow_remote_debug, :boolean, default: false
    field :metadata, :map, default: %{}

    belongs_to :organization, Organization

    timestamps(type: :utc_datetime)
  end

  @type t :: %__MODULE__{
          id: Ecto.UUID.t(),
          token: String.t(),
          name: String.t(),
          last_seen_at: DateTime.t() | nil,
          last_ip: String.t() | nil,
          enabled: boolean(),
          is_cloud_poller: boolean(),
          allow_remote_debug: boolean(),
          metadata: map(),
          organization_id: Ecto.UUID.t() | nil,
          organization: Ecto.Association.NotLoaded.t() | Organization.t(),
          inserted_at: DateTime.t(),
          updated_at: DateTime.t()
        }

  @doc """
  Builds a new agent token with cryptographically secure random bytes.

  Returns a tuple of {token_string, changeset}. The token_string is stored in the database
  and can be retrieved later for display to authorized users.

  For cloud pollers, pass `is_cloud_poller: true` in opts and organization_id will be nil.

  ## Examples

      iex> {token, changeset} = AgentToken.build_token(org_id, "My Agent")
      iex> is_binary(token)
      true
      iex> byte_size(Base.url_decode64!(token, padding: false))
      48

      iex> {token, changeset} = AgentToken.build_token(nil, "Cloud Poller", is_cloud_poller: true)
      iex> changeset.changes.is_cloud_poller
      true

  """
  def build_token(organization_id, name, opts \\ []) do
    is_cloud_poller = Keyword.get(opts, :is_cloud_poller, false)

    # Generate 48 bytes (384 bits) of cryptographically secure random data
    token = :crypto.strong_rand_bytes(@rand_size)
    encoded_token = Base.url_encode64(token, padding: false)

    attrs = %{
      organization_id: organization_id,
      name: name,
      token: encoded_token,
      is_cloud_poller: is_cloud_poller
    }

    changeset =
      %__MODULE__{}
      |> cast(attrs, [:organization_id, :name, :token, :is_cloud_poller])
      |> validate_cloud_poller_constraints()
      |> validate_required([:name, :token])
      |> unique_constraint(:token)

    {encoded_token, changeset}
  end

  defp validate_cloud_poller_constraints(changeset) do
    is_cloud_poller = get_field(changeset, :is_cloud_poller)
    organization_id = get_field(changeset, :organization_id)

    cond do
      is_cloud_poller && organization_id != nil ->
        add_error(changeset, :organization_id, "must be nil for cloud pollers")

      !is_cloud_poller && organization_id == nil ->
        add_error(changeset, :organization_id, "can't be blank for non-cloud pollers")

      true ->
        changeset
    end
  end

  @doc """
  Changeset for updating an agent token.

  Only allows updating the name and allow_remote_debug fields. Other fields like
  the token itself cannot be changed for security reasons.

  ## Examples

      iex> changeset = AgentToken.update_changeset(agent_token, %{name: "New Name"})
      iex> changeset.valid?
      true

      iex> changeset = AgentToken.update_changeset(agent_token, %{name: ""})
      iex> changeset.valid?
      false

  """
  def update_changeset(agent_token, attrs) do
    agent_token
    |> cast(attrs, [:name, :allow_remote_debug])
    |> validate_required([:name])
  end

  @doc """
  Verifies a token by checking if it exists in the database.

  Returns {:ok, token} if valid, otherwise {:error, :invalid_token}.

  ## Examples

      iex> AgentToken.verify_token("valid_token")
      {:ok, "valid_token"}

      iex> AgentToken.verify_token("invalid")
      {:error, :invalid_token}

  """
  def verify_token(token) when is_binary(token) do
    {:ok, token}
  end

  def verify_token(_), do: {:error, :invalid_token}
end
