lib/actors/actor/interface/http.ex

defmodule Actors.Actor.Interface.Http do
  @moduledoc """
  `Http` is responsible for the communication between the Proxy and the Host application
  when using the HTTP protocol.
  """
  use Actors.Actor.Interface
  require Logger

  alias Actors.{
    Actor.Entity.EntityState,
    Node.Client
  }

  alias Eigr.Functions.Protocol.Actors.{
    Actor,
    ActorId
  }

  alias Eigr.Functions.Protocol.{
    Context,
    ActorInvocation,
    ActorInvocationResponse
  }

  alias Google.Protobuf.Any

  @impl true
  def invoke_host(
        %ActorInvocation{
          command_name: command
        } = payload,
        %EntityState{
          actor: %Actor{commands: commands}
        } = state,
        default_actions
      ) do
    if Enum.member?(default_actions, command) and
         not Enum.any?(default_actions, fn action -> contains_action?(commands, action) end) do
      resp = do_invoke_default_action(payload, state)

      {:ok, resp, state}
    else
      do_invoke_host(payload, state)
    end
  end

  defp do_invoke_default_action(
         %ActorInvocation{
           actor: %ActorId{name: name, system: system},
           caller: caller
         } = _payload,
         %EntityState{
           actor: %Actor{state: actor_state, id: actor_id}
         } = _state
       ) do
    current_state = Map.get(actor_state || %{}, :state)
    current_tags = Map.get(actor_state || %{}, :tags, %{})

    context =
      if is_nil(current_state),
        do: %Context{caller: caller, self: actor_id, state: Any.new(), tags: current_tags},
        else: %Context{caller: caller, self: actor_id, state: current_state, tags: current_tags}

    ActorInvocationResponse.new(
      actor_name: name,
      actor_system: system,
      updated_context: context,
      payload: current_state
    )
  end

  defp do_invoke_host(payload, state) do
    payload
    |> ActorInvocation.encode()
    |> Client.invoke_host_actor()
    |> case do
      {:ok, %Tesla.Env{body: ""}} ->
        Logger.error("User Function Actor response Invocation body is empty")
        {:error, :no_content, state}

      {:ok, %Tesla.Env{body: nil}} ->
        Logger.error("User Function Actor response Invocation body is nil")
        {:error, :no_content, state}

      {:ok, %Tesla.Env{body: body}} ->
        case ActorInvocationResponse.decode(body) do
          %ActorInvocationResponse{
            updated_context: %Context{} = user_ctx
          } = resp ->
            {:ok, resp, update_state(state, user_ctx)}

          error ->
            Logger.error("Error on parse response #{inspect(error)}")
            {:error, :invalid_content, state}
        end

      {:error, reason} ->
        Logger.error("User Function Actor Invocation Unknown Error: #{inspect(reason)}")
        {:error, reason, state}
    end
  end

  defp contains_action?(commands, action), do: Enum.any?(commands, fn c -> c.name == action end)

  defp update_state(%EntityState{} = state, %Context{} = ctx) do
    actor = state.actor
    actor_state = actor.state

    if is_nil(actor_state) do
      state
    else
      new_actor_state =
        actor_state
        |> Map.put(:state, ctx.state || actor_state.state)
        |> Map.put(:tags, ctx.tags || actor_state.tags || %{})

      %{state | actor: %{actor | state: new_actor_state}}
    end
  end
end