defmodule BDS.Media.Linking do
  @moduledoc false

  require Logger

  import Ecto.Query

  alias BDS.Media.Media
  alias BDS.Media.Sidecars
  alias BDS.Persistence
  alias BDS.Posts.PostMedia
  alias BDS.Projects
  alias BDS.Repo

  @spec list_linked_posts(String.t()) ::
          [%{post_id: String.t(), title: String.t(), sort_order: integer()}]
  def list_linked_posts(media_id) when is_binary(media_id) do
    Repo.all(
      from post in BDS.Posts.Post,
        join: pm in PostMedia,
        on: pm.post_id == post.id,
        where: pm.media_id == ^media_id,
        order_by: [asc: pm.sort_order, asc: post.updated_at],
        select: %{
          post_id: post.id,
          title: fragment("COALESCE(?, ?, ?)", post.title, post.slug, post.id),
          sort_order: pm.sort_order
        }
    )
  end

  @spec link_media_to_post(String.t(), String.t()) ::
          {:ok, :linked} | {:error, :not_found | term()}
  def link_media_to_post(media_id, post_id) when is_binary(media_id) and is_binary(post_id) do
    case {Repo.get(Media, media_id), Repo.get(BDS.Posts.Post, post_id)} do
      {nil, _post} ->
        {:error, :not_found}

      {_media, nil} ->
        {:error, :not_found}

      {%Media{} = media, %BDS.Posts.Post{} = post} ->
        project = Projects.get_project!(media.project_id)

        case Repo.transaction(fn ->
               if Repo.exists?(
                    from pm in PostMedia,
                      where: pm.post_id == ^post.id and pm.media_id == ^media.id
                  ) do
                 :already_linked
               else
                 sort_order = next_sort_order(media.id)

                 %PostMedia{}
                 |> PostMedia.changeset(%{
                   id: Ecto.UUID.generate(),
                   project_id: media.project_id,
                   post_id: post.id,
                   media_id: media.id,
                   sort_order: sort_order,
                   created_at: Persistence.now_ms()
                 })
                 |> Repo.insert!()

                 :linked
               end
             end) do
          {:ok, _result} ->
            log_sidecar_error(Sidecars.write_sidecar(project, media), media.id)
            {:ok, :linked}

          {:error, reason} ->
            {:error, reason}
        end
    end
  end

  @spec unlink_media_from_post(String.t(), String.t()) ::
          {:ok, :unlinked} | {:error, :not_found | term()}
  def unlink_media_from_post(media_id, post_id) when is_binary(media_id) and is_binary(post_id) do
    case Repo.get(Media, media_id) do
      nil ->
        {:error, :not_found}

      %Media{} = media ->
        project = Projects.get_project!(media.project_id)

        case Repo.transaction(fn ->
               {_count, _} =
                 Repo.delete_all(
                   from pm in PostMedia,
                     where: pm.media_id == ^media.id and pm.post_id == ^post_id
                 )

               :ok
             end) do
          {:ok, :ok} ->
            log_sidecar_error(Sidecars.write_sidecar(project, media), media.id)
            {:ok, :unlinked}

          {:error, reason} ->
            {:error, reason}
        end
    end
  end

  @spec linked_post_ids(String.t()) :: [String.t()]
  def linked_post_ids(media_id) do
    Repo.all(
      from pm in PostMedia,
        where: pm.media_id == ^media_id,
        order_by: [asc: pm.sort_order, asc: pm.post_id],
        select: pm.post_id
    )
  end

  defp log_sidecar_error(:ok, _media_id), do: :ok

  defp log_sidecar_error({:error, reason}, media_id) do
    Logger.warning("Sidecar write failed for media #{media_id}: #{inspect(reason)}")
  end

  defp next_sort_order(media_id) do
    case Repo.one(
           from pm in PostMedia,
             where: pm.media_id == ^media_id,
             select: max(pm.sort_order)
         ) do
      value when is_integer(value) -> value + 1
      _other -> 0
    end
  end
end
