Skip to main content

lib/jido/chat/webhook_pipeline.ex

defmodule Jido.Chat.WebhookPipeline do
  @moduledoc false

  alias Jido.Chat.{Adapter, EventEnvelope, WebhookRequest, WebhookResponse}

  @type resolve_adapter_fun :: (map(), atom() -> {:ok, module()} | {:error, term()})
  @type process_event_fun ::
          (map(), atom(), EventEnvelope.t() | map(), keyword() ->
             {:ok, map(), EventEnvelope.t()} | {:error, term()})

  @spec handle_request(
          map(),
          atom(),
          WebhookRequest.t() | map(),
          keyword(),
          resolve_adapter_fun(),
          process_event_fun()
        ) ::
          {:ok, map(), EventEnvelope.t() | nil, WebhookResponse.t()}
  def handle_request(
        chat,
        adapter_name,
        request_or_payload,
        opts,
        resolve_adapter,
        process_event
      )
      when is_map(chat) and is_atom(adapter_name) and is_list(opts) and
             is_function(resolve_adapter, 2) and is_function(process_event, 4) do
    try do
      case resolve_adapter.(chat, adapter_name) do
        {:ok, adapter_module} ->
          request = normalize_webhook_request(adapter_name, request_or_payload, opts)
          request_opts = Keyword.put(opts, :request, request)

          case Adapter.verify_webhook(adapter_module, request, request_opts) do
            :ok ->
              handle_verified_webhook(
                chat,
                adapter_name,
                adapter_module,
                request,
                request_opts,
                process_event
              )

            {:error, reason} ->
              webhook_error_result(adapter_module, chat, nil, reason, request_opts)
          end

        {:error, {:unknown_adapter, unknown_adapter}} ->
          {:ok, chat, nil,
           WebhookResponse.error(404, %{
             error: "unknown_adapter",
             adapter_name: to_string(unknown_adapter)
           })}

        {:error, reason} ->
          {:ok, chat, nil, fallback_webhook_response(reason)}
      end
    rescue
      exception ->
        {:ok, chat, nil, fallback_webhook_response({:exception, exception})}
    end
  end

  defp handle_verified_webhook(
         chat,
         adapter_name,
         adapter_module,
         %WebhookRequest{} = request,
         request_opts,
         process_event
       ) do
    case Adapter.parse_event(adapter_module, request, request_opts) do
      {:ok, :noop} ->
        case Adapter.format_webhook_response(adapter_module, {:ok, chat, :noop}, request_opts) do
          {:ok, response} ->
            {:ok, chat, nil, response}

          {:error, reason} ->
            webhook_error_result(
              adapter_module,
              chat,
              nil,
              {:webhook_response_format_error, reason},
              request_opts
            )
        end

      {:ok, envelope} ->
        case process_event.(chat, adapter_name, envelope, request_opts) do
          {:ok, routed_chat, routed_envelope} ->
            case Adapter.format_webhook_response(
                   adapter_module,
                   {:ok, routed_chat, routed_envelope},
                   request_opts
                 ) do
              {:ok, response} ->
                {:ok, routed_chat, routed_envelope, response}

              {:error, reason} ->
                webhook_error_result(
                  adapter_module,
                  routed_chat,
                  routed_envelope,
                  {:webhook_response_format_error, reason},
                  request_opts
                )
            end

          {:error, reason} ->
            webhook_error_result(adapter_module, chat, envelope, reason, request_opts)
        end

      {:error, reason} ->
        webhook_error_result(adapter_module, chat, nil, reason, request_opts)
    end
  end

  defp webhook_error_result(adapter_module, chat, envelope, reason, opts) do
    case Adapter.format_webhook_response(adapter_module, {:error, reason}, opts) do
      {:ok, response} ->
        {:ok, chat, envelope, response}

      {:error, _format_error} ->
        {:ok, chat, envelope, fallback_webhook_response(reason)}
    end
  end

  defp fallback_webhook_response(:invalid_webhook_secret),
    do: WebhookResponse.error(401, %{error: "invalid_webhook_secret"})

  defp fallback_webhook_response(:invalid_signature),
    do: WebhookResponse.error(401, %{error: "invalid_signature"})

  defp fallback_webhook_response({:unknown_adapter, adapter_name}),
    do:
      WebhookResponse.error(404, %{
        error: "unknown_adapter",
        adapter_name: to_string(adapter_name)
      })

  defp fallback_webhook_response({:webhook_response_format_error, _reason}),
    do: WebhookResponse.error(500, %{error: "webhook_response_format_error"})

  defp fallback_webhook_response({:exception, exception}),
    do: WebhookResponse.error(500, %{error: "webhook_exception", reason: inspect(exception)})

  defp fallback_webhook_response(reason),
    do: WebhookResponse.error(400, %{error: "invalid_webhook_request", reason: inspect(reason)})

  defp normalize_webhook_request(adapter_name, %WebhookRequest{} = request, _opts) do
    %{request | adapter_name: request.adapter_name || adapter_name}
  end

  defp normalize_webhook_request(adapter_name, payload, opts) when is_map(payload) do
    payload_map = payload[:payload] || payload["payload"] || payload

    headers =
      opts[:headers] || payload[:headers] || payload["headers"] || %{}

    WebhookRequest.new(%{
      adapter_name: adapter_name,
      method: payload[:method] || payload["method"] || opts[:method] || "POST",
      path: payload[:path] || payload["path"] || opts[:path],
      headers: headers,
      payload: payload_map,
      query: payload[:query] || payload["query"] || opts[:query] || %{},
      raw: payload[:raw] || payload["raw"] || opts[:raw] || opts[:raw_body],
      metadata: payload[:metadata] || payload["metadata"] || %{}
    })
  end
end