lib/remedy/gateway/event_cache.ex

defmodule Remedy.Gateway.EventCache do
  import Ecto.Query, warn: false

  alias Sunbake.Snowflake
  alias Remedy.Cache.Repo
  alias Ecto.Changeset

  alias Remedy.Schema.{
    App,
    Ban,
    Channel,
    Emoji,
    Guild,
    Integration,
    Interaction,
    Invite,
    Member,
    Message,
    Role,
    Thread,
    User
  }

  @type snowflake :: Snowflake.t()
  @type reason :: String.t()
  @type attrs :: map()
  @type changeset :: Changeset.t()

  ###########
  ### Channels
  ###########

  # def update_thread_members

  @doc false
  @spec get_channel(snowflake) :: nil | Channel.t()
  def get_channel(id), do: Repo.get(Channel, id)

  @doc false
  @spec delete_channel(snowflake) :: {:ok, Channel.t()} | {:error, changeset}
  def delete_channel(id), do: get_channel(id) |> Repo.delete()

  @doc false
  @spec update_channel(attrs) :: {:ok, Channel.t()} | {:error, changeset}
  def update_channel(%{id: id} = attrs), do: update_channel(id, attrs)
  @doc false
  @spec update_channel(snowflake, attrs) :: {:ok, Channel.t()} | {:error, changeset}
  def update_channel(id, attrs) do
    case get_channel(id) do
      nil ->
        Channel.changeset(attrs) |> Repo.insert()

      %Channel{} = channel ->
        Channel.changeset(channel, attrs) |> Repo.update()
    end
  end

  ###########
  ### Threads
  ###########

  @doc false
  @spec get_thread(snowflake) :: nil | Thread.t()
  def get_thread(id), do: Repo.get(Thread, id)

  @doc false
  @spec delete_thread(snowflake) :: {:ok, Thread.t()} | {:error, changeset}
  def delete_thread(id), do: get_thread(id) |> Repo.delete()

  @doc false
  @spec update_thread(attrs) :: {:ok, Thread.t()} | {:error, changeset}
  def update_thread(%{id: id} = attrs), do: update_thread(id, attrs)
  @doc false
  @spec update_thread(snowflake, attrs) :: {:ok, Thread.t()} | {:error, changeset}
  def update_thread(id, attrs) do
    case get_thread(id) do
      nil ->
        Thread.changeset(attrs) |> Repo.insert()

      %Thread{} = thread ->
        Thread.changeset(thread, attrs) |> Repo.update()
    end
  end

  ###########
  ### Bans
  ###########

  @spec get_ban(snowflake, snowflake) :: nil | Ban.t()
  defp get_ban(guild_id, user_id), do: Repo.get_by(Ban, %{guild_id: guild_id, user_id: user_id})

  @doc false
  @spec delete_ban(snowflake, snowflake) :: {:ok, Ban.t()} | {:error, changeset}
  def delete_ban(guild_id, user_id), do: get_ban(guild_id, user_id) |> Repo.delete()

  @doc false
  @spec update_ban(attrs) :: {:ok, Ban.t()} | {:error, changeset}
  def update_ban(%{guild_id: guild_id, user_id: user_id} = attrs),
    do: get_ban(guild_id, user_id) |> Ban.changeset(attrs) |> Repo.update()

  ###########
  ### User
  ###########
  def update_user(attrs) do
    case get_user(attrs) do
      nil ->
        User.changeset(attrs)
        |> Repo.insert()

      %User{} = user ->
        User.changeset(user, attrs)
        |> Repo.update()
    end
  end

  @doc false
  def get_user(%{id: id}), do: Repo.get(User, id)
  def get_user(id), do: Repo.get(User, id)

  def delete_user(id) do
    User |> Repo.get(id) |> Repo.delete()
  end

  ###########
  ### Members
  ###########

  def get_member(guild_id, user_id), do: Repo.get_by(Member, %{guild_id: guild_id, user_id: user_id})

  def update_member(%{guild_id: guild_id, user_id: user_id} = attrs) do
    case get_member(guild_id, user_id) do
      nil -> Member.changeset(attrs) |> Repo.insert()
      %Member{} = member -> member |> Member.changeset(attrs) |> Repo.update()
    end
  end

  def update_member(%{guild_id: _guild_id, user: %{id: user_id}} = attrs) do
    Map.put_new(attrs, :user_id, user_id) |> update_member()
  end

  @doc false
  def update_presence(%{user: user} = presence), do: Map.put_new(user, :presence, presence) |> update_user()

  @doc false
  @spec update_guild(attrs) :: any
  def update_guild(%{id: id} = attrs) do
    case get_guild(id) do
      nil -> Guild.changeset(attrs) |> Repo.insert()
      %Guild{} = guild -> Guild.changeset(guild, attrs) |> Repo.update()
    end
  end

  @doc false
  @spec get_guild(snowflake) :: nil | Guild.t()
  def get_guild(guild_id), do: Repo.get(Guild, guild_id)

  @doc false
  @spec delete_guild(snowflake) :: {:ok, Guild.t()} | {:error, changeset}
  def delete_guild(guild_id), do: get_guild(guild_id) |> Repo.delete()

  @doc false
  @spec get_integration(snowflake) :: nil | Integration.t()
  def get_integration(integration_id), do: Repo.get(Integration, integration_id)

  @doc false
  @spec delete_integration(snowflake) :: {:ok, Integration.t()} | {:error, changeset}
  def delete_integration(integration_id), do: get_integration(integration_id) |> Repo.delete()

  @doc false
  @spec update_integration(attrs) :: {:ok, Integration.t()} | {:error, changeset}
  def update_integration(%{id: integration_id} = attrs) do
    case get_integration(integration_id) do
      nil -> Integration.changeset(attrs) |> Repo.insert()
      %Integration{} = integration -> Integration.changeset(integration, attrs) |> Repo.update()
    end
  end

  #################
  #### Interactions
  #################

  def create_interaction(attrs) do
    attrs
    |> Interaction.changeset()
    |> Repo.insert()
  end

  ############
  #### Invites
  ############

  @doc false
  @spec get_invite(snowflake) :: nil | Invite.t()
  def get_invite(invite_id), do: Repo.get(Invite, invite_id)

  @doc false
  @spec delete_invite(snowflake) :: {:ok, Invite.t()} | {:error, changeset}
  def delete_invite(invite_id), do: get_invite(invite_id) |> Repo.delete()

  @doc false
  def update_invite(%{id: id} = attrs) do
    case Repo.get(Invite, id) do
      nil -> Invite.changeset(attrs) |> Repo.insert()
      %Invite{} = invite -> Invite.changeset(invite, attrs) |> Repo.update()
    end
  end

  #############
  #### Message
  #############

  @doc false
  def update_message(%{id: id} = attrs) do
    case Repo.get(Message, id) do
      nil -> Message.changeset(attrs) |> Repo.insert()
      %Message{} = message -> Message.changeset(message, attrs) |> Repo.update()
    end
  end

  @doc false
  @spec get_message(snowflake) :: nil | Message.t()
  def get_message(message_id), do: Repo.get(Message, message_id)

  @doc false
  @spec delete_message(snowflake) :: {:ok, Message.t()} | {:error, changeset}
  def delete_message(message_id) do
    get_message(message_id) |> Repo.delete()
  end

  @doc false
  @spec update_message(snowflake, attrs) :: {:ok, Message.t()} | {:error, changeset}
  def update_message(message_id, attrs) do
    get_message(message_id) |> Message.changeset(attrs) |> Repo.update()
  end

  @spec remove_message_reactions(snowflake) :: {:ok, Message.t()} | {:error, reason}
  def remove_message_reactions(message_id) do
    get_message(message_id) |> Message.changeset(%{reactions: []}) |> Repo.update()
  end

  #########
  ### Emoji
  #########

  @doc false
  @spec get_emoji(snowflake) :: nil | Emoji.t()
  def get_emoji(emoji_id), do: Repo.get(Emoji, emoji_id)

  @doc false
  @spec delete_emoji(snowflake) :: {:ok, Emoji.t()} | {:error, changeset}
  def delete_emoji(emoji_id), do: get_emoji(emoji_id) |> Repo.delete()

  @doc false
  def update_emoji(%{id: id} = attrs) do
    case Repo.get(Emoji, id) do
      nil -> Emoji.changeset(attrs) |> Repo.insert()
      %Emoji{} = emoji -> Emoji.changeset(emoji, attrs) |> Repo.update()
    end
  end

  #########
  ### Roles
  #########

  @doc false
  @spec update_role(attrs) :: {:ok, Role.t()} | {:error, changeset}
  def update_role(%{id: role_id} = attrs) do
    case get_role(role_id) do
      nil -> Role.changeset(attrs) |> Repo.insert()
      %Role{} = role -> Role.changeset(role, attrs) |> Repo.update()
    end
  end

  @doc false
  @spec get_role(snowflake) :: nil | Role.t()
  def get_role(id), do: Repo.get(Role, id)

  @doc false
  @spec delete_role(snowflake) :: {:ok, Role.t()} | {:error, changeset}
  def delete_role(id), do: get_role(id) |> Repo.delete()

  @doc false
  def init_bot(bot) do
    clear_bot_cache()

    User.system_changeset(bot)
    |> Repo.insert()
  end

  def bot, do: Repo.get_by(User, %{remedy_system: true})

  defp clear_bot_cache() do
    case bot() do
      %User{} = user -> Repo.delete(user)
      nil -> :noop
    end

    :ok
  end

  @doc false
  def init_app(app) do
    clear_app_cache()

    App.system_changeset(app)
    |> Repo.insert()
  end

  def app, do: Repo.get_by(App, %{remedy_system: true})

  defp clear_app_cache do
    case app() do
      %App{} = app -> Repo.delete(app)
      nil -> :noop
    end

    :ok
  end

  alias Ecto.Association.NotLoaded

  @spec drop_unloaded_assoc(map) :: any
  def drop_unloaded_assoc(%{} = map), do: drop_by(map, fn _, val -> val in [%NotLoaded{}] end)

  @spec drop_metadata(map) :: any
  def drop_metadata(%{} = map), do: drop_by(map, fn key, _ -> key in [:__meta__] end)

  defp drop_by(struct, _predicate) when is_struct(struct), do: struct
  defp drop_by(map, predicate) when is_map(map), do: clean_by(map, predicate)
  defp drop_by(list, predicate) when is_list(list), do: Enum.map(list, &drop_by(&1, predicate))
  defp drop_by(elem, _predicate), do: elem

  defp clean_by(map, predicate) do
    Enum.reduce(map, %{}, fn {key, val}, acc ->
      if predicate.(key, drop_by(val, predicate)) do
        acc
      else
        Map.put(acc, key, drop_by(val, predicate))
      end
    end)
  end

  defp wrap_list({:error, _reason} = error), do: error
  defp wrap_list([]), do: {:ok, []}
  defp wrap_list([_ | _] = list), do: {:ok, list}

  defp wrap(%{} = struct), do: {:ok, struct}
  defp wrap(nil), do: {:error, @not_found}

  defp unwrap({:ok, body}), do: body
  defp unwrap({:error, _}), do: raise("Cache Error")
end