if Code.ensure_loaded?(Plug) do
  defmodule Guardian.Plug.VerifyHeader do
    @moduledoc """
    Looks for and validates a token found in the `Authorization` header.

    In the case where either:

    1. A token is already found for `:key`
    2. No token is found in the `Authorization` header.

    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:

    * `claims` - The literal claims to check to ensure that a token is valid
    * `max_age` - If the token has an "auth_time" claim, check it is not older than the maximum age.
    * `header_name` - The name of the header to search for a token. Defaults to `authorization`.
    * `scheme` - The prefix for the token in the header. Defaults to `Bearer`.
      `:none` will not use a prefix.
    * `key` - The location to store the information in the connection. Defaults to: `default`
    * `halt` - Whether to halt the connection in case of error. Defaults to `true`.
    * `: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`

    ### Example

    ```elixir
    # setup the upstream pipeline

    plug Guardian.Plug.VerifyHeader, claims: %{typ: "access"}
    ```

    This will check the authorization header for a token

    `Authorization: Bearer <token>`

    This token will be placed into the connection depending on the key and can be accessed with
    `Guardian.Plug.current_token` and `Guardian.Plug.current_claims`.

    OR

    `MyApp.ImplementationModule.current_token` and `MyApp.ImplementationModule.current_claims`.
    """

    alias Guardian.Plug.Pipeline

    import Plug.Conn

    @behaviour Plug

    @impl Plug
    @spec init(opts :: Keyword.t()) :: Keyword.t()
    def init(opts \\ []) do
      opts
      |> get_scheme()
      |> put_scheme_reg(opts)
    end

    @impl Plug
    @spec call(Plug.Conn.t(), Keyword.t()) :: Plug.Conn.t()
    def call(conn, opts) do
      with nil <- Guardian.Plug.current_token(conn, opts),
           {:ok, token} <- fetch_token_from_header(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 put_scheme_reg("", opts) do
      opts
    end

    defp put_scheme_reg(:none, opts) do
      opts
    end

    defp put_scheme_reg(scheme, opts) do
      Keyword.put(opts, :scheme_reg, "#{scheme}\:?\s+(.*)$")
    end

    defp get_scheme(opts) do
      if Keyword.has_key?(opts, :realm) do
        IO.warn("`:realm` option is deprecated; please rename `:realm` to `:scheme` option instead.")
        Keyword.get(opts, :realm, "Bearer")
      else
        Keyword.get(opts, :scheme, "Bearer")
      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

    @spec fetch_token_from_header(Plug.Conn.t(), Keyword.t()) ::
            :no_token_found
            | {:ok, String.t()}
    defp fetch_token_from_header(conn, opts) do
      header_name = Keyword.get(opts, :header_name, "authorization")
      headers = get_req_header(conn, header_name)
      fetch_token_from_header(conn, opts, headers)
    end

    @spec fetch_token_from_header(Plug.Conn.t(), Keyword.t(), Keyword.t()) ::
            :no_token_found
            | {:ok, String.t()}
    defp fetch_token_from_header(_, _, []), do: :no_token_found

    defp fetch_token_from_header(conn, opts, [token | tail]) do
      reg =
        case Keyword.get(opts, :scheme_reg, ~r/^(.*)$/) do
          %Regex{} = reg -> reg
          reg_str -> Regex.compile!(reg_str, "i")
        end

      trimmed_token = String.trim(token)

      case Regex.run(reg, trimmed_token) do
        [_, match] -> {:ok, String.trim(match)}
        _ -> fetch_token_from_header(conn, opts, tail)
      end
    end

    @spec storage_key(Plug.Conn.t(), Keyword.t()) :: String.t()
    defp storage_key(conn, opts), do: Pipeline.fetch_key(conn, opts)
  end
end
