defmodule Guardian.Token.Verify do
  @moduledoc """
  Interface for verifying tokens.

  This is intended to be used primarily by token modules but allows for a
  custom verification module to be created if the one that ships with your
  TokenModule is not quite what you want.
  """

  @doc """
  Verify a single claim.

  You should also include a fallback for claims that you are not validating.

  ```elixir
  def verify_claim(_mod, _key, claims, _opts), do: {:ok, claims}
  ```

  """
  @callback verify_claim(
              mod :: module,
              claim_key :: String.t(),
              claims :: Guardian.Token.claims(),
              options :: Guardian.options()
            ) :: {:ok, Guardian.Token.claims()} | {:error, atom}

  defmacro __using__(_opts \\ []) do
    quote do
      def verify_claims(mod, claims, opts) do
        Enum.reduce(claims, {:ok, claims}, fn
          {k, v}, {:ok, claims} -> verify_claim(mod, k, claims, opts)
          _, {:error, reason} = err -> err
        end)
      end

      def verify_claim(_mod, _claim_key, claims, _opts), do: {:ok, claims}

      defoverridable verify_claim: 4
    end
  end

  @spec time_within_drift?(mod :: module, time :: pos_integer | float) :: true | false
  @doc """
  Checks that a time value is within the `allowed_drift` as
  configured for the provided module.

  Allowed drift is measured in seconds and represents the maximum amount of
  time a token may be expired for an still be considered valid.

  This is to deal with clock skew.
  """
  def time_within_drift?(mod, time) when is_integer(time) or is_float(time) do
    allowed_drift = apply(mod, :config, [:allowed_drift, 0]) / 1000
    diff = abs(time - Guardian.timestamp())
    diff <= allowed_drift
  end

  def time_within_drift?(_, _), do: true

  @spec verify_literal_claims(
          claims :: Guardian.Token.claims(),
          claims_to_check :: Guardian.Token.claims() | nil,
          opts :: Guardian.options()
        ) :: {:ok, Guardian.Token.claims()} | {:error, any}
  @doc """
  For claims, check the values against the values found in
  `claims_to_check`. If there is a claim to check that does not pass
  verification, it fails.

  When the value of a claim is a list, it checks that all values of
  the same claim in `claims_to_check` are members of the list.
  """
  def verify_literal_claims(claims, nil, _opts), do: {:ok, claims}

  def verify_literal_claims(claims, claims_to_check, _opts) do
    errors =
      Enum.reduce(claims_to_check, [], fn {k, v}, acc ->
        case verify_literal_claim(claims, k, v) do
          {:ok, _} -> acc
          error -> [error | acc]
        end
      end)

    if Enum.empty?(errors) do
      {:ok, claims}
    else
      hd(errors)
    end
  end

  @spec verify_literal_claim(map(), binary(), [binary()] | binary()) ::
          {:ok, [binary()] | binary()} | {:error, binary()}
  defp verify_literal_claim(claims, key, value) do
    claim_value = Map.get(claims, key)

    if valid_claims?(claim_value, value) do
      {:ok, claim_value}
    else
      {:error, key}
    end
  end

  defp valid_claims?(claim_values, valid) when is_list(claim_values) and is_list(valid) do
    Enum.all?(valid, &(&1 in claim_values))
  end

  defp valid_claims?(claim_values, valid) when is_list(claim_values), do: valid in claim_values

  defp valid_claims?(claim_value, valid), do: claim_value == valid
end
