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

defmodule AshAuthentication.AddOn.Confirmation.Actions do
  @moduledoc """
  Actions for the confirmation add-on.

  Provides the code interface for working with resources via confirmation.
  """

  alias Ash.{Changeset, Error.Framework.AssumptionFailed, Query, Resource}

  alias AshAuthentication.{
    AddOn.Confirmation,
    Errors.InvalidToken,
    Info,
    Jwt,
    Strategy,
    TokenResource
  }

  @doc """
  Attempt to confirm a user.
  """
  @spec confirm(Confirmation.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
  def confirm(strategy, params, opts \\ []) do
    with {:ok, token} <- Map.fetch(params, "confirm"),
         {:ok, %{"sub" => subject}, _} <- Jwt.verify(token, strategy.resource, opts),
         {:ok, user} <- AshAuthentication.subject_to_user(subject, strategy.resource, opts),
         {:ok, token_resource} <- Info.authentication_tokens_token_resource(strategy.resource) do
      opts =
        opts
        |> Keyword.put_new_lazy(:domain, fn ->
          Info.domain!(strategy.resource)
        end)

      user
      |> Changeset.new()
      |> Changeset.set_context(%{
        private: %{
          ash_authentication?: true
        }
      })
      |> Changeset.for_update(strategy.confirm_action_name, params, opts)
      |> Changeset.after_action(fn _changeset, record ->
        case TokenResource.revoke(token_resource, token, opts) do
          :ok -> {:ok, record}
          {:error, reason} -> {:error, reason}
        end
      end)
      |> Ash.update()
    else
      :error -> {:error, InvalidToken.exception(type: :confirmation)}
      {:error, reason} -> {:error, reason}
    end
  end

  @doc """
  Store changes in the tokens resource for later re-use.
  """
  @spec store_changes(Confirmation.t(), String.t(), Changeset.t(), keyword) :: :ok | {:error, any}
  def store_changes(strategy, token, changeset, opts \\ []) do
    changes =
      strategy.monitor_fields
      |> Stream.filter(&Changeset.changing_attribute?(changeset, &1))
      |> Enum.reduce_while({:ok, %{}}, fn field, {:ok, acc} ->
        if Keyword.has_key?(changeset.atomics, field) do
          {:halt,
           {:error,
            "Cannot store the changes to the field #{field} because it is being updated atomically."}}
        else
          {:cont,
           {:ok, Map.put(acc, field, to_string(Changeset.get_attribute(changeset, field)))}}
        end
      end)

    with {:ok, changes} <- changes,
         {:ok, token_resource} <- Info.authentication_tokens_token_resource(strategy.resource),
         {:ok, domain} <- TokenResource.Info.token_domain(token_resource),
         opts <- opts |> Keyword.put(:upsert?, true) |> Keyword.put_new(:domain, domain),
         {:ok, store_changes_action} <-
           TokenResource.Info.token_confirmation_store_changes_action_name(token_resource),
         {:ok, _token_record} <-
           token_resource
           |> Changeset.new()
           |> Changeset.set_context(%{
             private: %{
               ash_authentication?: true
             }
           })
           |> Changeset.for_create(
             store_changes_action,
             %{
               token: token,
               extra_data: changes,
               purpose: to_string(Strategy.name(strategy))
             },
             opts
           )
           |> Ash.create() do
      :ok
    else
      {:error, reason} ->
        {:error, reason}

      :error ->
        {:error,
         AssumptionFailed.exception(
           message: "Configuration error storing confirmation token data"
         )}
    end
  end

  @doc """
  Get changes from the tokens resource for application.
  """
  @spec get_changes(Confirmation.t(), String.t(), keyword) :: {:ok, map} | :error
  def get_changes(strategy, jti, opts \\ []) do
    with {:ok, token_resource} <- Info.authentication_tokens_token_resource(strategy.resource),
         opts <-
           Keyword.put_new_lazy(opts, :domain, fn ->
             TokenResource.Info.token_domain!(token_resource)
           end),
         {:ok, get_changes_action} <-
           TokenResource.Info.token_confirmation_get_changes_action_name(token_resource),
         {:ok, [token_record]} <-
           token_resource
           |> Query.new()
           |> Query.set_context(%{
             private: %{
               ash_authentication?: true
             }
           })
           |> Query.set_context(%{strategy: strategy})
           |> Query.for_read(get_changes_action, %{"jti" => jti})
           |> Ash.read(opts) do
      changes =
        strategy.monitor_fields
        |> Stream.map(&to_string/1)
        |> Stream.map(&{&1, Map.get(token_record.extra_data, &1)})
        |> Stream.reject(&is_nil(elem(&1, 1)))
        |> Map.new()

      {:ok, changes}
    else
      _ -> :error
    end
  end
end
