defmodule Raxx.Session.SignedCookie do
  @moduledoc """
  Stores the session in a signed cookie.

  **Note: the contents of the cookie can be viewed by the client,
  an encrypted cookie store must be used to hide data from client.
  See `Raxx.Session.EncryptedCookie`.**

  This module is not normally used directly.
  Instead see `Raxx.Session` and use this module as the `:store` option.

  ## Options

  - `:secret_key_base` - used to generate the signing key
  - `:salt` - a salt used with `secret_key_base` to generate a key for signing/verifying a cookie.

  TODO support key generation options, currently sensible defaults are used.
  """
  @enforce_keys [:secret_key_base, :salt]
  defstruct @enforce_keys

  def config(options) do
    {secret_key_base, options} = Keyword.pop(options, :secret_key_base)

    secret_key_base ||
      raise ArgumentError, "#{__MODULE__} requires the :secret_key_base option to be set"

    validate_secret_key_base(secret_key_base)

    {salt, _options} = Keyword.pop(options, :salt)
    salt || raise ArgumentError, "#{__MODULE__} requires the :salt option to be set"

    %__MODULE__{secret_key_base: secret_key_base, salt: salt}
  end

  defp validate_secret_key_base(secret_key_base) when byte_size(secret_key_base) < 64 do
    raise(ArgumentError, "#{__MODULE__} requires `secret_key_base` to be at least 64 bytes")
  end

  defp validate_secret_key_base(secret_key_base), do: secret_key_base

  def fetch(session_string, config = %__MODULE__{}) do
    case Plug.Crypto.MessageVerifier.verify(session_string, key(config)) do
      {:ok, binary} ->
        {:ok, Plug.Crypto.safe_binary_to_term(binary)}

      :error ->
        {:error, :invalid_signature}
    end
  end

  def put(session, config = %__MODULE__{}) do
    binary = :erlang.term_to_binary(session)
    Plug.Crypto.MessageVerifier.sign(binary, key(config))
  end

  defp key(%__MODULE__{secret_key_base: secret_key_base, salt: salt}) do
    Plug.Crypto.KeyGenerator.generate(secret_key_base, salt)
  end
end
