defmodule Erlef.Accounts do
  @moduledoc """
  Erlef.Accounts context module

  ## Types of accounts

  - Member

  ### Membership levels

   - basic
   - annual supporting members
   - lifetime
   - fellow
   - board
  """

  alias Ecto.Multi
  alias Erlef.Repo
  alias Erlef.Accounts.{External, Member}

  defdelegate to_member_params(member, params), to: External
  defdelegate to_member(member, params), to: External

  @spec get_member(Ecto.UUID.t()) :: Member.t() | nil
  def get_member(id), do: Repo.get(Member, id)

  @spec get_member!(Ecto.UUID.t()) :: Member.t()
  def get_member!(id), do: Repo.get!(Member, id)

  @spec get_member_by_external_id(String.t() | integer()) :: Member.t() | nil
  @doc """
  Gets a member by its associated external id. 

  See `Erlef.Accounts.External` for more details on how fields are mapped between
  this application and an external resource.
  """
  def get_member_by_external_id(id) when is_integer(id),
    do: get_member_by_external_id(Integer.to_string(id))

  def get_member_by_external_id(id), do: Repo.one(Member.by_external_id(id))

  @spec update_member(Member.t(), map()) :: {:ok, map()} | {:error, term()}
  @doc """
  Updates a member record and its external source. 

  This function will attempt to it's associated external
  source, followed by the record in the database, all within a transaction. 

  See `Erlef.Accounts.External` for more details on how fields are mapped between
  this application and an external resource.
  """
  def update_member(member, params, opts \\ []) do
    res =
      Multi.new()
      |> Multi.update(:member, Member.changeset(member, params))
      |> Multi.run(:contact, fn _, _ -> maybe_update_external(member, params, opts) end)
      |> Repo.transaction()

    case res do
      {:ok, %{member: member}} -> {:ok, member}
      {:error, :member, err, _} -> {:error, err}
      err -> err
    end
  end

  defp maybe_update_external(member, params, update_external: true) do
    update_external(member, params)
  end

  defp maybe_update_external(_, _, _), do: {:ok, :noop}

  @spec create_member(map()) :: {:ok, Member.t()} | {:error, Ecto.Changeset.t()}
  @doc """
  Creates a new member record. 

  This function will attempt to create a member. If no id is provided 
  the associated external resource will be updated with the autogenerated id. 

  See `Erlef.Accounts.External` for more details on how fields are mapped between
  this application and an external resource.
  """
  def create_member(attrs \\ %{}) do
    res =
      %Member{}
      |> Member.changeset(attrs)
      |> Repo.insert()

    case res do
      {:ok, member} -> maybe_sync_member_id(member, attrs)
      err -> err
    end
  end

  defp update_external(%Member{external: %{id: id}} = member, params) do
    contact = External.to_external(member, params)
    Erlef.WildApricot.update_contact(id, contact)
  end

  defp maybe_sync_member_id(member, params) do
    case Map.get(params, :id) do
      nil ->
        {:ok, _} = update_external(member, %{id: member.id})
        {:ok, member}

      _ ->
        {:ok, member}
    end
  end
end
