lib/phoenix_llm_chat/core.ex

defmodule PhoenixLLMChat.Core do
  @moduledoc """
  Core event handling for chat flow.

  Manages:
  - Message input and chat submission
  - View state switching
  - Message lifecycle (send, receive, persist)
  - Response finalization and formatting
  """

  require Logger
  alias PhoenixLLMChat.{StreamRuntime, LLMContext, Utilities}

  def mount(socket, session) do
    socket
    |> Phoenix.Component.assign(:messages, [])
    |> Phoenix.Component.assign(:input, "")
    |> Phoenix.Component.assign(:loading, false)
    |> Phoenix.Component.assign(:error, nil)
    |> Phoenix.Component.assign(:response, nil)
    |> Phoenix.Component.assign(:current_request_ref, nil)
    |> Phoenix.Component.assign(:session_id, session["session_id"] || generate_session_id())
  end

  def handle_event("update_chat_input", %{"value" => value}, socket) do
    {:noreply, Phoenix.Component.assign(socket, :input, value)}
  end

  def handle_event("send_message", _params, socket) do
    input = socket.assigns.input
    session_id = socket.assigns.session_id

    case String.trim(input) do
      "" ->
        {:noreply, socket}

      trimmed_input ->
        socket
        |> add_message(%{"role" => "user", "content" => trimmed_input, "session_id" => session_id})
        |> Phoenix.Component.assign(:input, "")
        |> Phoenix.Component.assign(:loading, true)
        |> Phoenix.Component.assign(:response, nil)
        |> Phoenix.Component.assign(:error, nil)
        |> submit_to_llm(trimmed_input)
        |> case do
          {:ok, updated_socket} -> {:noreply, updated_socket}
          {:error, reason} ->
            {:noreply, Phoenix.Component.assign(socket, :error, "Failed to send: #{inspect(reason)}")}
        end
    end
  end

  def handle_event("set_chat_view", %{"view" => view}, socket) do
    {:noreply, Phoenix.Component.assign(socket, :current_view, view)}
  end

  def handle_event("clear_chat", _params, socket) do
    {:noreply, Phoenix.Component.assign(socket, :messages, [])}
  end

  def handle_info({:llm_stream_delta, request_ref, delta}, socket) do
    case socket.assigns[:current_request_ref] do
      ^request_ref -> {:noreply, StreamRuntime.handle_llm_delta(socket, request_ref, delta)}
      _ -> {:noreply, socket}
    end
  end

  def handle_info({:llm_stream_done, request_ref, metadata}, socket) do
    case socket.assigns[:current_request_ref] do
      ^request_ref -> {:noreply, finalize_response(socket, request_ref, metadata)}
      _ -> {:noreply, socket}
    end
  end

  def handle_info({:llm_stream_error, request_ref, error}, socket) do
    case socket.assigns[:current_request_ref] do
      ^request_ref -> {:noreply, StreamRuntime.handle_llm_error(socket, request_ref, error)}
      _ -> {:noreply, socket}
    end
  end

  def handle_info({:llm_stream_trace, request_ref, trace}, socket) do
    case socket.assigns[:current_request_ref] do
      ^request_ref -> {:noreply, StreamRuntime.handle_llm_trace(socket, request_ref, trace)}
      _ -> {:noreply, socket}
    end
  end

  def handle_info({ref, result}, socket) when is_reference(ref) do
    {:noreply, StreamRuntime.handle_task_ref_cleanup(socket, {ref, result})}
  end

  def handle_info({:DOWN, ref, :process, _pid, _reason}, socket) do
    {:noreply, StreamRuntime.handle_process_down(socket, {:DOWN, ref, :process, nil, nil})}
  end

  def terminate(_reason, socket) do
    StreamRuntime.cleanup_on_terminate(socket)
  end

  defp add_message(socket, message) do
    messages = socket.assigns.messages || []
    Phoenix.Component.assign(socket, :messages, messages ++ [message])
  end

  defp submit_to_llm(socket, user_message) do
    case LLMContext.call_llm(socket, user_message) do
      {:ok, updated_socket} -> {:ok, updated_socket}
      {:error, reason} -> {:error, reason}
    end
  end

  defp finalize_response(socket, request_ref, metadata) do
    response = socket.assigns.response || ""

    socket
    |> add_message(%{
      "role" => "assistant",
      "content" => response,
      "request_ref" => request_ref,
      "metadata" => metadata
    })
    |> apply_message_filters(response)
    |> StreamRuntime.handle_llm_done(request_ref, metadata)
  end

  defp apply_message_filters(socket, response) do
    case Application.get_env(:phoenix_llm_chat, :response_filters) do
      nil -> socket
      filters -> Phoenix.Component.assign(socket, :response, Utilities.apply_message_filters(response, filters))
    end
  end

  defp generate_session_id do
    Base.encode16(:crypto.strong_rand_bytes(8))
  end
end