if Code.ensure_loaded?(Plug) do
  defmodule Guardian.Plug.VerifySession do
    @moduledoc """
    Looks for and validates a token found in the session.

    In the case where either:

    1. The session is not loaded
    2. A token is already found for `:key`
    3. No token is found on the session

    This plug will not do anything.

    This, like all other Guardian plugs, requires a Guardian pipeline to be setup.
    It requires an implementation module, an error handler and a key.

    These can be set either:

    1. Upstream on the connection with `plug Guardian.Pipeline`
    2. Upstream on the connection with `Guardian.Pipeline.{put_module, put_error_handler, put_key}`
    3. Inline with an option of `:module`, `:error_handler`, `:key`

    If a token is found but is invalid, the error handler will be called with
    `auth_error(conn, {:invalid_token, reason}, opts)`.

    Once a token has been found it will be decoded, the token and claims will
    be put onto the connection.

    They will be available using `Guardian.Plug.current_claims/2` and `Guardian.Plug.current_token/2`.

    Options:

    * `:refresh_from_cookie` - Looks for and validates a token found in the request cookies. (default `false`)

    Refresh from cookie option

    * `:key` - The location of the token (default `:default`)
    * `:exchange_from` - The type of the cookie (default `"refresh"`)
    * `:exchange_to` - The type of token to provide. Defaults to the
      implementation modules `default_type`
    * `:ttl` - The time to live of the exchanged token. Defaults to configured values.
    * `:halt` - Whether to halt the connection in case of error. Defaults to `true`
    """

    import Plug.Conn
    import Guardian.Plug.Keys

    alias Guardian.Plug.Pipeline

    @behaviour Plug

    @impl Plug
    @spec init(opts :: Keyword.t()) :: Keyword.t()
    def init(opts), do: opts

    @impl Plug
    @spec call(conn :: Plug.Conn.t(), opts :: Keyword.t()) :: Plug.Conn.t()
    def call(conn, opts) do
      if Guardian.Plug.session_active?(conn) do
        verify_session(conn, opts)
      else
        conn
      end
    end

    defp verify_session(conn, opts) do
      with nil <- Guardian.Plug.current_token(conn, opts),
           {:ok, token} <- find_token_from_session(conn, opts),
           module <- Pipeline.fetch_module!(conn, opts),
           claims_to_check <- Keyword.get(opts, :claims, %{}),
           key <- storage_key(conn, opts),
           {:ok, claims} <- Guardian.decode_and_verify(module, token, claims_to_check, opts) do
        conn
        |> Guardian.Plug.put_current_token(token, key: key)
        |> Guardian.Plug.put_current_claims(claims, key: key)
      else
        error ->
          handle_error(conn, error, opts)
      end
    end

    defp handle_error(conn, error, opts) do
      if refresh_from_cookie_opts = fetch_refresh_from_cookie_options(opts) do
        Guardian.Plug.VerifyCookie.refresh_from_cookie(conn, refresh_from_cookie_opts)
      else
        apply_error(conn, error, opts)
      end
    end

    defp apply_error(conn, {:error, reason}, opts) do
      conn
      |> Pipeline.fetch_error_handler!(opts)
      |> apply(:auth_error, [conn, {:invalid_token, reason}, opts])
      |> Guardian.Plug.maybe_halt(opts)
    end

    defp apply_error(conn, _, _) do
      conn
    end

    defp fetch_refresh_from_cookie_options(opts) do
      case Keyword.get(opts, :refresh_from_cookie) do
        value when is_list(value) -> value
        true -> []
        _ -> nil
      end
    end

    defp find_token_from_session(conn, opts) do
      key = conn |> storage_key(opts) |> token_key()
      token = get_session(conn, key)
      if token, do: {:ok, token}, else: :no_token_found
    end

    defp storage_key(conn, opts), do: Pipeline.fetch_key(conn, opts)
  end
end
