# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT

defmodule AshAuthentication.Plug do
  @moduledoc ~S"""
  Generate an authentication plug.

  Use in your app by creating a new module called `AuthPlug` or similar:

  ```elixir
  defmodule MyAppWeb.AuthPlug do
    use AshAuthentication.Plug, otp_app: :my_app

    def handle_success(conn, _activity, user, _token) do
      conn
      |> store_in_session(user)
      |> send_resp(200, "Welcome back #{user.name}")
    end

    def handle_failure(conn, _activity, reason) do
      conn
      |> send_resp(401, "Better luck next time")
    end
  end
  ```

  ### Using in Phoenix

  In your Phoenix router you can add it:

  ```elixir
  scope "/auth" do
    pipe_through :browser
    forward "/", MyAppWeb.AuthPlug
  end
  ```

  In order to load any authenticated users for either web or API users you can add the following to your router:

  ```elixir
  import MyAppWeb.AuthPlug

  pipeline :session_users do
    plug :load_from_session
  end

  pipeline :bearer_users do
    plug :load_from_bearer
  end

  scope "/", MyAppWeb do
    pipe_through [:browser, :session_users]

    live "/", PageLive, :home
  end

  scope "/api", MyAppWeb do
    pipe_through [:api, :bearer_users]

    get "/" ApiController, :index
  end
  ```
  ### Using in a Plug application

  ```elixir
  use Plug.Router

  forward "/auth", to: MyAppWeb.AuthPlug
  ```

  Note that you will need to include a bunch of other plugs in the pipeline to
  do useful things like session and query param fetching.
  """

  alias Ash.Resource
  alias AshAuthentication.Plug.{Defaults, Helpers, Macros}
  alias Plug.Conn
  require Macros

  @type activity :: {atom, atom}
  @type token :: String.t()

  @doc """
  When authentication has been succesful, this callback will be called with the
  conn, the successful activity, the authenticated resource and a token.

  This allows you to choose what action to take as appropriate for your
  application.

  The default implementation calls `store_in_session/2` and returns a simple
  "Access granted" message to the user.  You almost definitely want to override
  this behaviour.
  """
  @callback handle_success(Conn.t(), activity, Resource.record() | nil, token | nil) :: Conn.t()

  @doc """
  When there is any failure during authentication this callback is called.

  Note that this includes not just authentication failures but potentially
  route-not-found errors also.

  The default implementation simply returns a 401 status with the message
  "Access denied".  You almost definitely want to override this.
  """
  @callback handle_failure(Conn.t(), activity, any) :: Conn.t()

  @doc false
  @spec __using__(keyword) :: Macro.t()
  defmacro __using__(opts) do
    otp_app =
      opts
      |> Keyword.fetch!(:otp_app)
      |> Macro.expand_once(__CALLER__)

    quote do
      require Macros
      Macros.validate_subject_name_uniqueness(unquote(otp_app))

      @behaviour AshAuthentication.Plug
      @behaviour Plug
      import Plug.Conn

      defmodule Router do
        @moduledoc false
        use AshAuthentication.Plug.Router,
          otp_app: unquote(otp_app),
          return_to:
            __MODULE__
            |> Module.split()
            |> List.delete_at(-1)
            |> Module.concat()
      end

      Macros.define_sign_in_with_remember_me(unquote(otp_app))
      Macros.define_load_from_session(unquote(otp_app))
      Macros.define_load_from_bearer(unquote(otp_app))
      Macros.define_revoke_bearer_tokens(unquote(otp_app))

      @impl true
      defdelegate handle_success(conn, activity, user, token), to: Defaults

      @impl true
      defdelegate handle_failure(conn, activity, error), to: Defaults

      defoverridable handle_success: 4, handle_failure: 3

      @impl true
      defdelegate init(opts), to: Router

      @impl true
      defdelegate call(conn, opts), to: Router

      defdelegate set_actor(conn, subject_name), to: Helpers
      defdelegate store_in_session(conn, user), to: Helpers
    end
  end
end
