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

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

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

  alias Google.Protobuf.Any

  @http_node_client Application.compile_env(:spawn, :http_node_client, Actors.Node.Client)

  @impl true
  def invoke_host(
        %ActorInvocation{
          action_name: command
        } = payload,
        %EntityState{
          actor: %Actor{actions: actions}
        } = state,
        default_actions
      ) do
    if Enum.member?(default_actions, command) and
         not Enum.any?(default_actions, fn action -> contains_action?(actions, 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{}, tags: current_tags},
        else: %Context{caller: caller, self: actor_id, state: current_state, tags: current_tags}

    %ActorInvocationResponse{
      actor_name: name,
      actor_system: system,
      updated_context: context,
      payload: {:value, current_state}
    }
  end

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

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

      {:ok, %Finch.Response{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?(actions, action), do: Enum.any?(actions, 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