defmodule MusicLibrary.Chats do
  @moduledoc """
  Persistent storage for AI chat conversations about records and artists.
  """

  import Ecto.Query, warn: false

  alias MusicLibrary.Chats.{Chat, Message}
  alias MusicLibrary.Repo

  @spec list_chats(atom(), String.t()) :: [Chat.t()]
  def list_chats(entity, musicbrainz_id) do
    message_count_query =
      from(m in Message,
        where: m.chat_id == parent_as(:chat).id,
        select: count(m.id)
      )

    from(c in Chat,
      as: :chat,
      where: c.entity == ^entity and c.musicbrainz_id == ^musicbrainz_id,
      order_by: [desc: c.updated_at],
      select_merge: %{message_count: subquery(message_count_query)}
    )
    |> Repo.all()
  end

  @spec count_chats(atom(), String.t()) :: non_neg_integer()
  def count_chats(entity, musicbrainz_id) do
    from(c in Chat,
      where: c.entity == ^entity and c.musicbrainz_id == ^musicbrainz_id
    )
    |> Repo.aggregate(:count)
  end

  @spec has_any_chats?(atom(), String.t()) :: boolean()
  def has_any_chats?(entity, musicbrainz_id) do
    from(c in Chat,
      where: c.entity == ^entity and c.musicbrainz_id == ^musicbrainz_id
    )
    |> Repo.exists?()
  end

  @spec get_chat!(String.t()) :: Chat.t()
  def get_chat!(id) do
    Chat
    |> Repo.get!(id)
    |> Repo.preload(:messages)
  end

  @spec create_chat_with_message(map(), map()) ::
          {:ok, Chat.t()} | {:error, Ecto.Changeset.t()}
  def create_chat_with_message(chat_attrs, message_attrs) do
    topic =
      message_attrs
      |> Map.get(:content, "")
      |> String.trim()
      |> String.slice(0, 80)

    Repo.transaction(fn ->
      chat_attrs = Map.put(chat_attrs, :topic, topic)

      with {:ok, chat} <- %Chat{} |> Chat.changeset(chat_attrs) |> Repo.insert(),
           message_attrs = Map.merge(message_attrs, %{position: 0}),
           {:ok, _message} <-
             %Message{chat_id: chat.id}
             |> Message.changeset(message_attrs)
             |> Repo.insert() do
        Repo.preload(chat, :messages)
      else
        {:error, changeset} -> Repo.rollback(changeset)
      end
    end)
  end

  @spec add_message(Chat.t(), map()) :: {:ok, Message.t()} | {:error, Ecto.Changeset.t()}
  def add_message(%Chat{} = chat, attrs) do
    next_position =
      from(m in Message,
        where: m.chat_id == ^chat.id,
        select: coalesce(max(m.position), -1)
      )
      |> Repo.one!()
      |> Kernel.+(1)

    attrs = Map.put(attrs, :position, next_position)

    result =
      %Message{chat_id: chat.id}
      |> Message.changeset(attrs)
      |> Repo.insert()

    case result do
      {:ok, message} ->
        touch_chat(chat)
        {:ok, message}

      error ->
        error
    end
  end

  @spec delete_chat(Chat.t()) :: {:ok, Chat.t()} | {:error, Ecto.Changeset.t()}
  def delete_chat(%Chat{} = chat) do
    Repo.delete(chat)
  end

  defp touch_chat(chat) do
    now = DateTime.truncate(DateTime.utc_now(), :second)

    from(c in Chat, where: c.id == ^chat.id)
    |> Repo.update_all(set: [updated_at: now])
  end
end
