defmodule MusicLibraryWeb.Components.Release do
  use MusicLibraryWeb, :live_component

  require Logger

  alias MusicBrainz.Release
  alias MusicLibrary.Records.TracklistPdf
  alias MusicLibrary.ScrobbleActivity
  alias MusicLibraryWeb.Duration
  alias MusicLibraryWeb.ErrorMessages
  alias Phoenix.LiveView.AsyncResult

  def open(id), do: Fluxon.open_dialog(id)

  @impl true
  def mount(socket) do
    {:ok,
     socket
     |> assign(:can_scrobble?, ScrobbleActivity.can_scrobble?())
     |> assign(:release_with_tracks, AsyncResult.loading())
     |> assign(:already_scrobbled, false)
     |> assign(:selected_tracks, MapSet.new())}
  end

  @impl true
  def update(%{record: record} = assigns, socket) do
    {:ok,
     socket
     |> assign(assigns)
     |> assign_async(:release_with_tracks, fn ->
       with {:ok, release} <- load_release_with_tracks(record.selected_release_id) do
         {:ok, %{release_with_tracks: release}}
       end
     end)}
  end

  def update(%{already_scrobbled: _already_scrobbled} = assigns, socket) do
    {:ok, assign(socket, assigns)}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.sheet
        :if={@record.selected_release_id}
        id={@sheet_id}
        placement="right"
        class="min-w-xs sm:min-w-sm"
      >
        <div class="mt-6 flex items-center justify-between gap-4">
          <label class="cursor-pointer text-lg font-semibold text-zinc-700 dark:text-zinc-300">
            <input
              :if={
                @can_scrobble? && @release_with_tracks.ok? &&
                  Release.media_count(@release_with_tracks.result) == 1
              }
              type="checkbox"
              id="medium-checkbox-1"
              checked={
                medium_selected?(
                  Release.get_medium(@release_with_tracks.result, 1),
                  @selected_tracks
                )
              }
              phx-click="toggle_medium"
              phx-value-medium-number={1}
              phx-target={@myself}
              class="mr-2 size-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600"
            />
            {gettext("Tracks")}
          </label>
          <.button_group>
            <.button
              :if={@release_with_tracks.ok?}
              variant="soft"
              size="sm"
              phx-click="print_tracklist"
              phx-target={@myself}
            >
              <span class="sr-only">{gettext("Print tracklist")}</span>
              <.icon name="hero-printer" class="icon" aria-hidden="true" data-slot="icon" />
            </.button>
            <.button
              :if={@can_scrobble? && @release_with_tracks.ok?}
              variant="soft"
              size="sm"
              disabled={@already_scrobbled}
              phx-click={
                if MapSet.size(@selected_tracks) > 0,
                  do: "scrobble_selected_tracks",
                  else: "scrobble_release"
              }
              phx-target={@myself}
              phx-disable-with={gettext("Scrobbling...")}
            >
              <span class="sr-only">{scrobble_button_label(@selected_tracks)}</span>
              <.icon name="hero-play" class="icon" aria-hidden="true" data-slot="icon" />
            </.button>
            <.button :if={!@can_scrobble?} variant="soft" size="sm" href={LastFm.auth_url()}>
              <span class="sr-only">{gettext("Connect your Last.fm account")}</span>
              <.icon name="hero-link" class="icon" aria-hidden="true" data-slot="icon" />
            </.button>
          </.button_group>
        </div>

        <div :if={@release_with_tracks} class="mt-4 space-y-4">
          <.async_result :let={release_with_tracks} assign={@release_with_tracks}>
            <:loading>
              <div class="mt-48 flex items-center justify-center">
                <span class="sr-only">{gettext("Loading release with tracks")}</span>
                <.loading />
              </div>
            </:loading>
            <:failed :let={_failure}>
              <div class="mt-4 text-sm/5 text-zinc-500 dark:text-zinc-400">
                <.icon
                  name="hero-exclamation-triangle"
                  class="size-5"
                  aria-hidden="true"
                  data-slot="icon"
                />
                {gettext("Error loading tracks")}
                <.button
                  variant="ghost"
                  size="xs"
                  phx-click={JS.push("load_release_tracks", target: @myself)}
                  class="ml-2 cursor-pointer"
                >
                  {gettext("Retry")}
                </.button>
              </div>
            </:failed>
            <.medium
              :for={medium <- release_with_tracks.media}
              can_scrobble?={@can_scrobble?}
              already_scrobbled={@already_scrobbled}
              medium={medium}
              release_artists={release_with_tracks.artists}
              media_count={MusicBrainz.Release.media_count(release_with_tracks)}
              selected_tracks={@selected_tracks}
              myself={@myself}
              record={@record}
            />
          </.async_result>
        </div>
      </.sheet>
    </div>
    """
  end

  attr :medium, Release.Medium, required: true
  attr :release_artists, :list, required: true
  attr :media_count, :integer, required: true
  attr :can_scrobble?, :boolean, required: true
  attr :already_scrobbled, :boolean, required: true
  attr :selected_tracks, :any, required: true
  attr :myself, :any, required: true
  attr :record, :any, default: nil

  def medium(assigns) do
    ~H"""
    <div
      :if={@media_count > 1}
      class="flex items-center justify-between gap-4"
    >
      <label class="md:text-md cursor-pointer text-sm font-semibold text-zinc-700 dark:text-zinc-300">
        <input
          :if={@can_scrobble?}
          type="checkbox"
          id={"medium-checkbox-#{@medium.number}"}
          checked={medium_selected?(@medium, @selected_tracks)}
          phx-click="toggle_medium"
          phx-value-medium-number={@medium.number}
          phx-target={@myself}
          class="mr-2 size-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600"
        />
        {medium_title(@medium)}
        <.badge variant="soft" class="text-xs">
          {@medium.format}
        </.badge>
      </label>
      <.button_group>
        <.button
          :if={@record}
          variant="soft"
          size="sm"
          phx-click="print_medium_tracklist"
          phx-value-medium-number={@medium.number}
          phx-target={@myself}
        >
          <span class="sr-only">{gettext("Print tracklist")}</span>
          <.icon name="hero-printer" class="icon" aria-hidden="true" data-slot="icon" />
        </.button>
        <.button
          :if={@can_scrobble?}
          variant="soft"
          size="sm"
          disabled={@already_scrobbled || MapSet.size(@selected_tracks) > 0}
          phx-click="scrobble_medium"
          phx-value-number={@medium.number}
          phx-target={@myself}
          phx-disable-with={gettext("Scrobbling...")}
        >
          <span class="sr-only">{medium_scrobble_label(@medium.format)}</span>
          <.icon name="hero-play" class="icon" aria-hidden="true" data-slot="icon" />
        </.button>
      </.button_group>
    </div>
    <.track_list
      medium_number={@medium.number}
      tracks={@medium.tracks}
      release_artists={@release_artists}
      selected_tracks={@selected_tracks}
      can_scrobble?={@can_scrobble?}
      myself={@myself}
    />
    <.separator />
    <p class="text-right text-xs text-zinc-700 md:text-sm dark:text-zinc-300">
      {medium_duration(@medium)}
    </p>
    """
  end

  defp medium_scrobble_label(format) do
    if format && String.contains?(format, "Vinyl") do
      gettext("Scrobble side")
    else
      gettext("Scrobble disc")
    end
  end

  attr :medium_number, :integer, required: true
  attr :tracks, :list, required: true
  attr :release_artists, :list, required: true
  attr :selected_tracks, :any, required: true
  attr :can_scrobble?, :boolean, required: true
  attr :myself, :any, required: true

  def track_list(assigns) do
    ~H"""
    <ul id={"disc-#{@medium_number}"} class="table w-full table-auto">
      <li
        :for={track <- @tracks}
        class="list-none leading-5 text-zinc-700 dark:text-zinc-300 md:mt-4"
      >
        <label class="contents cursor-pointer">
          <div class="table-row">
            <span :if={@can_scrobble?} class="table-cell pr-2 align-middle">
              <input
                type="checkbox"
                id={"track-checkbox-#{track.id}"}
                checked={MapSet.member?(@selected_tracks, track.id)}
                phx-click="toggle_track"
                phx-value-track-id={track.id}
                phx-target={@myself}
                class="size-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600"
              />
            </span>
            <span class="table-cell pr-1 text-right text-xs text-nowrap md:text-sm">
              {track.number || track.position}
            </span>
            <span class="table-cell w-full text-xs/8 font-normal md:text-sm">
              {track.title}
            </span>
            <span class="table-cell pl-2 text-right text-xs md:text-sm">
              {track.length && Duration.format_duration(track.length)}
            </span>
          </div>
          <div
            :if={@release_artists !== track.artists}
            class="table-row text-xs md:text-sm"
          >
            <span :if={@can_scrobble?} class="table-cell" />
            <span class="table-cell" />
            <span class="table-cell">
              {Enum.map_join(track.artists, ", ", fn artist -> artist.name end)}
            </span>
          </div>
        </label>
      </li>
    </ul>
    """
  end

  defguardp release_loaded?(assigns) when assigns.release_with_tracks.ok?

  @impl true
  def handle_event("scrobble_release", _params, socket) when release_loaded?(socket.assigns) do
    release_with_tracks = socket.assigns.release_with_tracks.result

    case ScrobbleActivity.scrobble_release(release_with_tracks, :finished_at, DateTime.utc_now()) do
      {:ok, _} ->
        send_update_after(socket.assigns.myself, %{already_scrobbled: false}, 3000)
        put_toast!(:info, gettext("Release scrobbled successfully"))

        {:noreply, socket |> assign(:already_scrobbled, true)}

      {:error, reason} ->
        Logger.error("Error scrobbling release: #{inspect(reason)}")

        put_toast!(
          :error,
          gettext("Error scrobbling release") <> ": " <> ErrorMessages.friendly_message(reason)
        )

        {:noreply, socket}
    end
  end

  def handle_event("scrobble_release", _params, socket) do
    put_toast!(:error, gettext("Error scrobbling release"))
    {:noreply, socket}
  end

  def handle_event("scrobble_medium", %{"number" => number}, socket)
      when release_loaded?(socket.assigns) do
    release_with_tracks = socket.assigns.release_with_tracks.result
    {number, ""} = Integer.parse(number)

    case ScrobbleActivity.scrobble_medium(
           number,
           release_with_tracks,
           :finished_at,
           DateTime.utc_now()
         ) do
      {:ok, _} ->
        send_update_after(socket.assigns.myself, %{already_scrobbled: false}, 3000)
        put_toast!(:info, gettext("Disc scrobbled successfully"))

        {:noreply, socket |> assign(:already_scrobbled, true)}

      {:error, reason} ->
        Logger.error("Error scrobbling medium: #{inspect(reason)}")

        put_toast!(
          :error,
          gettext("Error scrobbling disc") <> ": " <> ErrorMessages.friendly_message(reason)
        )

        {:noreply, socket}
    end
  end

  def handle_event("scrobble_medium", _params, socket) do
    put_toast!(:error, gettext("Error scrobbling disc"))
    {:noreply, socket}
  end

  def handle_event("toggle_track", %{"track-id" => track_id}, socket) do
    selected_tracks = socket.assigns.selected_tracks

    updated_tracks =
      if MapSet.member?(selected_tracks, track_id) do
        MapSet.delete(selected_tracks, track_id)
      else
        MapSet.put(selected_tracks, track_id)
      end

    {:noreply, assign(socket, :selected_tracks, updated_tracks)}
  end

  def handle_event("toggle_medium", %{"medium-number" => number}, socket) do
    {number, ""} = Integer.parse(number)
    selected_tracks = socket.assigns.selected_tracks
    release_with_tracks = socket.assigns.release_with_tracks.result

    medium_tracks =
      release_with_tracks
      |> Release.medium_tracks(number)
      |> Enum.map(& &1.id)
      |> MapSet.new()

    updated_tracks =
      if MapSet.subset?(medium_tracks, selected_tracks) do
        MapSet.difference(selected_tracks, medium_tracks)
      else
        MapSet.union(selected_tracks, medium_tracks)
      end

    {:noreply, assign(socket, :selected_tracks, updated_tracks)}
  end

  def handle_event("scrobble_selected_tracks", _params, socket)
      when release_loaded?(socket.assigns) do
    release_with_tracks = socket.assigns.release_with_tracks.result
    selected_track_ids = socket.assigns.selected_tracks

    if MapSet.size(selected_track_ids) == 0 do
      put_toast!(:error, gettext("No tracks selected"))
      {:noreply, socket}
    else
      case ScrobbleActivity.scrobble_tracks(
             selected_track_ids,
             release_with_tracks,
             :finished_at,
             DateTime.utc_now()
           ) do
        {:ok, _} ->
          send_update_after(socket.assigns.myself, %{already_scrobbled: false}, 3000)
          put_toast!(:info, gettext("Selected tracks scrobbled successfully"))

          {:noreply,
           socket
           |> assign(:already_scrobbled, true)
           |> assign(:selected_tracks, MapSet.new())}

        {:error, reason} ->
          Logger.error("Error scrobbling tracks: #{inspect(reason)}")

          put_toast!(
            :error,
            gettext("Error scrobbling selected tracks") <>
              ": " <> ErrorMessages.friendly_message(reason)
          )

          {:noreply, socket}
      end
    end
  end

  def handle_event("scrobble_selected_tracks", _params, socket) do
    put_toast!(:error, gettext("Error scrobbling selected tracks"))
    {:noreply, socket}
  end

  def handle_event("print_tracklist", _params, socket) when release_loaded?(socket.assigns) do
    release = socket.assigns.release_with_tracks.result
    record = socket.assigns.record

    case TracklistPdf.generate(record, release) do
      {:ok, pdf_binary} ->
        filename = "#{record.title} - Tracklist.pdf"

        {:noreply,
         push_event(socket, "music_library:download", %{
           data: Base.encode64(pdf_binary),
           filename: filename,
           content_type: "application/pdf"
         })}

      {:error, reason} ->
        Logger.error("Error generating tracklist PDF: #{inspect(reason)}")

        put_toast!(
          :error,
          gettext("Error generating tracklist PDF") <>
            ": " <> ErrorMessages.friendly_message(reason)
        )

        {:noreply, socket}
    end
  end

  def handle_event("print_tracklist", _params, socket) do
    {:noreply, socket}
  end

  def handle_event("print_medium_tracklist", %{"medium-number" => number}, socket)
      when release_loaded?(socket.assigns) do
    release = socket.assigns.release_with_tracks.result
    record = socket.assigns.record
    {number, ""} = Integer.parse(number)

    case TracklistPdf.generate_medium(record, release, number) do
      {:ok, pdf_binary} ->
        filename = "#{record.title} - Disc #{number} - Tracklist.pdf"

        {:noreply,
         push_event(socket, "music_library:download", %{
           data: Base.encode64(pdf_binary),
           filename: filename,
           content_type: "application/pdf"
         })}

      {:error, reason} ->
        Logger.error("Error generating medium tracklist PDF: #{inspect(reason)}")

        put_toast!(
          :error,
          gettext("Error generating tracklist PDF") <>
            ": " <> ErrorMessages.friendly_message(reason)
        )

        {:noreply, socket}
    end
  end

  def handle_event("print_medium_tracklist", _params, socket) do
    {:noreply, socket}
  end

  def handle_event("load_release_tracks", _params, socket) do
    selected_release_id = socket.assigns.record.selected_release_id

    {:noreply,
     socket
     |> assign_async(:release_with_tracks, fn ->
       with {:ok, release} <- load_release_with_tracks(selected_release_id) do
         {:ok, %{release_with_tracks: release}}
       end
     end)}
  end

  defp load_release_with_tracks(release_id) do
    with {:ok, release} <- MusicBrainz.get_release(release_id) do
      {:ok, MusicBrainz.Release.from_api_response(release)}
    end
  end

  defp medium_selected?(medium, selected_tracks) do
    medium_track_ids = medium.tracks |> Enum.map(& &1.id) |> MapSet.new()
    MapSet.subset?(medium_track_ids, selected_tracks)
  end

  def scrobble_button_label(selected_tracks) do
    if MapSet.size(selected_tracks) > 0 do
      gettext("Scrobble selected tracks")
    else
      gettext("Scrobble release")
    end
  end

  defp medium_duration(medium) do
    medium
    |> MusicBrainz.Release.medium_duration()
    |> Duration.format_duration()
  end

  defp medium_title(medium) do
    if medium.title === "" do
      gettext("Disc %{no}", %{no: medium.number})
    else
      medium.title
    end
  end
end
