lib/actors/actor/interface/http.ex

defmodule Actors.Actor.Interface.Http do
  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{
          actor: %ActorId{name: name, system: system},
          command_name: command,
          caller: caller
        } = payload,
        %EntityState{
          actor: %Actor{state: actor_state, id: actor_id, commands: commands}
        } = state,
        default_actions
      ) do
    if Enum.member?(default_actions, command) and
         not Enum.any?(default_actions, fn action ->
           Enum.any?(commands, fn c -> c.name == action end)
         end) 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}

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

      {:ok, resp, state}
    else
      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}} ->
          with %ActorInvocationResponse{
                 updated_context: %Context{} = user_ctx
               } = resp <- ActorInvocationResponse.decode(body) do
            {:ok, resp, update_state(state, user_ctx)}
          else
            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
  end

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

    cond do
      is_nil(actor_state) ->
        state

      true ->
        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