defmodule MusicLibrary.ScrobbleActivity do
  import Ecto.Query

  alias LastFm.{Scrobble, Track}
  alias MusicBrainz.Release
  alias MusicLibrary.{Artists, Collection, Repo, Secrets, Wishlist}

  def can_scrobble? do
    Secrets.get("last_fm_session_key") !== nil
  end

  def scrobble_release(release_with_tracks, opts) when is_list(opts) do
    case Enum.sort(opts) do
      [finished_at: _, started_at: _] ->
        raise ArgumentError, """
        Cannot scobble a release with both started_at and finished_at.
          Remove either of them.
        """

      [started_at: started_at] ->
        scrobble_release(release_with_tracks, {:started_at, started_at})

      [finished_at: finished_at] ->
        scrobble_release(release_with_tracks, {:finished_at, finished_at})
    end
  end

  def scrobble_release(release_with_tracks, {:finished_at, finished_at}) do
    release_duration = Release.release_duration(release_with_tracks)
    started_at = DateTime.add(finished_at, -release_duration, :millisecond)
    scrobble_release(release_with_tracks, {:started_at, started_at})
  end

  def scrobble_release(release_with_tracks, {:started_at, started_at}) do
    session_key = Secrets.get!("last_fm_session_key").value

    {scrobbles, _finished_at} =
      release_with_tracks
      |> MusicBrainz.Release.tracks()
      |> to_scrobbles(release_with_tracks, started_at)

    LastFm.scrobble(scrobbles, session_key)
  end

  def scrobble_medium(number, release_with_tracks, opts) when is_list(opts) do
    case Enum.sort(opts) do
      [finished_at: _, started_at: _] ->
        raise ArgumentError, """
        Cannot scobble a medium with both started_at and finished_at.
          Remove either of them.
        """

      [started_at: started_at] ->
        scrobble_medium(number, release_with_tracks, {:started_at, started_at})

      [finished_at: finished_at] ->
        scrobble_medium(number, release_with_tracks, {:finished_at, finished_at})
    end
  end

  def scrobble_medium(number, release_with_tracks, {:finished_at, finished_at}) do
    medium_duration =
      release_with_tracks.media
      |> Enum.find(fn medium -> medium.number == number end)
      |> Release.medium_duration()

    started_at = DateTime.add(finished_at, -medium_duration, :millisecond)
    scrobble_medium(number, release_with_tracks, {:started_at, started_at})
  end

  def scrobble_medium(number, release_with_tracks, {:started_at, started_at}) do
    session_key = Secrets.get!("last_fm_session_key").value

    medium =
      release_with_tracks.media
      |> Enum.find(fn medium -> medium.number == number end)

    {scrobbles, _finished_at} =
      medium.tracks
      |> to_scrobbles(release_with_tracks, started_at)

    LastFm.scrobble(scrobbles, session_key)
  end

  defp to_scrobbles(tracks, release_with_tracks, started_at) do
    tracks
    |> Enum.map_reduce(started_at, fn track, time ->
      album_artist =
        if release_with_tracks.artists !== track.artists do
          main_artist_name(release_with_tracks.artists)
        end

      time = time |> DateTime.add(track.length, :millisecond)

      scrobble = %Scrobble{
        artist: main_artist_name(track.artists),
        album: release_with_tracks.title,
        album_artist: album_artist,
        track: track.title,
        timestamp: DateTime.to_unix(time)
      }

      {scrobble, time}
    end)
  end

  defp main_artist_name([]), do: nil
  defp main_artist_name([artist | _rest]), do: artist.name

  def scrobble_count do
    Repo.aggregate(Track, :count, :scrobbled_at_uts)
  end

  def from_recent_tracks(recent_tracks, timezone) do
    all_artist_pairs = Artists.get_all_artist_pairs()
    recent_release_ids = recent_release_ids(recent_tracks)
    collected_releases = Collection.collected_releases(recent_release_ids)
    wishlisted_releases = Wishlist.wishlisted_releases(recent_release_ids)

    localized_recent_tracks =
      Enum.map(recent_tracks, fn t ->
        %{
          t
          | scrobbled_at_label: localize_scrobbled_at(t.scrobbled_at_uts, timezone),
            artist: polyfill_artist(t, collected_releases, wishlisted_releases, all_artist_pairs)
        }
      end)

    all_artist_ids = Artists.get_all_artist_ids()
    recent_artist_ids = recent_artist_ids(localized_recent_tracks)
    artist_ids = MapSet.intersection(all_artist_ids, recent_artist_ids)

    recent_albums =
      localized_recent_tracks
      |> Enum.dedup_by(fn t -> t.album end)
      |> Enum.map(fn t ->
        %{
          scrobbled_at_uts: t.scrobbled_at_uts,
          scrobbled_at_label: t.scrobbled_at_label,
          metadata: t.album,
          artist: t.artist,
          cover_url: t.cover_url
        }
      end)

    %{
      localized_recent_tracks: localized_recent_tracks,
      localized_recent_albums: recent_albums,
      collected_releases: collected_releases,
      wishlisted_releases: wishlisted_releases,
      artist_ids: artist_ids
    }
  end

  defp polyfill_artist(track, collected_releases, wishlisted_releases, all_artist_pairs) do
    %{
      track.artist
      | musicbrainz_id:
          find_artist_id(track, collected_releases, wishlisted_releases, all_artist_pairs)
    }
  end

  defguardp has_no_artist_id(track)
            when is_nil(track.artist.musicbrainz_id) or track.artist.musicbrainz_id == ""

  defp find_artist_id(track, collected_releases, wishlisted_releases, all_artist_pairs)
       when has_no_artist_id(track) do
    matched_release =
      Enum.find(collected_releases ++ wishlisted_releases, fn r ->
        r.release_id == track.album.musicbrainz_id
      end)

    record_id = if matched_release, do: matched_release.record_id

    if record_id do
      Enum.find_value(all_artist_pairs, fn pair ->
        if pair.record_id == record_id, do: pair.artist_id
      end)
    end
  end

  defp find_artist_id(track, _collected_releases, _wishlisted_releases, _all_artist_pairs) do
    track.artist.musicbrainz_id
  end

  def localize_scrobbled_at(uts, timezone) do
    ldt =
      uts
      |> DateTime.from_unix!()
      |> DateTime.shift_zone!(timezone)

    Calendar.strftime(ldt, "%d/%m/%Y %X")
  end

  defp recent_release_ids(recent_tracks) do
    recent_tracks
    |> Enum.map(fn t -> t.album.musicbrainz_id end)
    |> Enum.uniq()
    |> Enum.reject(fn musicbrainz_id -> musicbrainz_id == "" end)
  end

  defp recent_artist_ids(recent_tracks) do
    recent_tracks
    |> Enum.map(fn t -> t.artist.musicbrainz_id end)
    |> Enum.uniq()
    |> Enum.reject(fn musicbrainz_id -> musicbrainz_id == "" end)
    |> MapSet.new()
  end

  @doc """
  Gets the top albums by scrobble count for the given number of days.
  Returns a list of maps with album information and play counts.
  """
  def get_top_albums_by_days(days, opts) do
    limit = Keyword.get(opts, :limit, 10)
    current_time = Keyword.get_lazy(opts, :current_time, &DateTime.utc_now/0)
    timezone = Keyword.get(opts, :timezone, &MusicLibrary.default_timezone/0)

    cutoff_timestamp =
      current_time
      |> DateTime.add(-days, :day)
      |> NaiveDateTime.beginning_of_day()
      |> DateTime.from_naive!(timezone)
      |> DateTime.to_unix()

    query =
      from t in Track,
        where: t.scrobbled_at_uts >= ^cutoff_timestamp,
        where: fragment("json_extract(album, '$.title') != ''"),
        group_by: [
          fragment("json_extract(album, '$.title')"),
          fragment("json_extract(artist, '$.name')")
        ],
        select: %{
          album_title: fragment("json_extract(album, '$.title')"),
          artist_name: fragment("json_extract(artist, '$.name')"),
          artist_musicbrainz_id: fragment("json_extract(artist, '$.musicbrainz_id')"),
          play_count: count(t.scrobbled_at_uts),
          cover_url: fragment("max(?)", t.cover_url),
          album_musicbrainz_id: fragment("json_extract(album, '$.musicbrainz_id')")
        },
        order_by: [desc: count(t.scrobbled_at_uts)],
        limit: ^limit

    Repo.all(query)
  end

  @doc """
  Gets the top albums by scrobble count across all time.
  Returns a list of maps with album information and play counts.
  """
  def get_top_albums(opts) do
    limit = Keyword.get(opts, :limit, 10)

    query =
      from t in Track,
        where: fragment("json_extract(album, '$.title') != ''"),
        group_by: [
          fragment("json_extract(album, '$.title')"),
          fragment("json_extract(artist, '$.name')")
        ],
        select: %{
          album_title: fragment("json_extract(album, '$.title')"),
          artist_name: fragment("json_extract(artist, '$.name')"),
          artist_musicbrainz_id: fragment("json_extract(artist, '$.musicbrainz_id')"),
          play_count: count(t.scrobbled_at_uts),
          cover_url: fragment("max(?)", t.cover_url),
          album_musicbrainz_id: fragment("json_extract(album, '$.musicbrainz_id')")
        },
        order_by: [desc: count(t.scrobbled_at_uts)],
        limit: ^limit

    Repo.all(query)
  end

  @doc """
  Gets the top artists by scrobble count across all time.
  Returns a list of maps with artist information and play counts.
  """
  def get_top_artists(opts) do
    limit = Keyword.get(opts, :limit, 10)

    query =
      from t in Track,
        join: ai in Artists.ArtistInfo,
        on: ai.id == fragment("json_extract(?, '$.musicbrainz_id')", t.artist),
        group_by: [
          fragment("json_extract(artist, '$.name')"),
          fragment("json_extract(artist, '$.musicbrainz_id')")
        ],
        select: %{
          name: fragment("json_extract(artist, '$.name')"),
          musicbrainz_id: fragment("json_extract(artist, '$.musicbrainz_id')"),
          image_hash: ai.image_data_hash,
          play_count: count(t.scrobbled_at_uts)
        },
        order_by: [desc: count(t.scrobbled_at_uts)],
        limit: ^limit

    Repo.all(query)
  end

  @doc """
  Gets the top artists by scrobble count for the given number of days.
  Returns a list of maps with artist information and play counts.
  """
  def get_top_artists_by_days(days, opts) do
    limit = Keyword.get(opts, :limit, 10)
    current_time = Keyword.get_lazy(opts, :current_time, &DateTime.utc_now/0)
    timezone = Keyword.get(opts, :timezone, &MusicLibrary.default_timezone/0)

    cutoff_timestamp =
      current_time
      |> DateTime.add(-days, :day)
      |> NaiveDateTime.beginning_of_day()
      |> DateTime.from_naive!(timezone)
      |> DateTime.to_unix()

    query =
      from t in Track,
        join: ai in Artists.ArtistInfo,
        on: ai.id == fragment("json_extract(?, '$.musicbrainz_id')", t.artist),
        where: t.scrobbled_at_uts >= ^cutoff_timestamp,
        group_by: [
          fragment("json_extract(artist, '$.name')"),
          fragment("json_extract(artist, '$.musicbrainz_id')")
        ],
        select: %{
          name: fragment("json_extract(artist, '$.name')"),
          musicbrainz_id: fragment("json_extract(artist, '$.musicbrainz_id')"),
          image_hash: ai.image_data_hash,
          play_count: count(t.scrobbled_at_uts)
        },
        order_by: [desc: count(t.scrobbled_at_uts)],
        limit: ^limit

    Repo.all(query)
  end

  @doc """
  Gets top albums for the specified time periods (30, 90, 365 days) and all
  time. Returns a map with the results, along with collected and wishlisted
  releases.
  """
  def get_top_albums_by_period(opts) do
    period = Keyword.get(opts, :period, :last_30_days)

    albums =
      case period do
        :all_time -> get_top_albums(opts)
        :last_30_days -> get_top_albums_by_days(30, opts)
        :last_90_days -> get_top_albums_by_days(90, opts)
        :last_365_days -> get_top_albums_by_days(365, opts)
      end

    all_album_ids =
      albums
      |> Enum.map(fn t -> t.album_musicbrainz_id end)
      |> Enum.uniq()
      |> Enum.reject(fn musicbrainz_id -> musicbrainz_id == "" end)

    collected_releases = Collection.collected_releases(all_album_ids)
    wishlisted_releases = Wishlist.wishlisted_releases(all_album_ids)

    %{
      collected_releases: collected_releases,
      wishlisted_releases: wishlisted_releases,
      albums: albums
    }
  end

  @doc """
  Gets top artists for a time period (30, 90, 365 days) and all time.
  """
  def get_top_artists_by_period(opts) do
    period = Keyword.get(opts, :period, :last_30_days)

    case period do
      :all_time -> get_top_artists(opts)
      :last_30_days -> get_top_artists_by_days(30, opts)
      :last_90_days -> get_top_artists_by_days(90, opts)
      :last_365_days -> get_top_artists_by_days(365, opts)
    end
  end

  @doc """
  Lists scrobbled tracks with pagination and search support.
  """
  def list_tracks(params \\ %{}) do
    query = Map.get(params, :query, "")
    page = Map.get(params, :page, 1)
    page_size = Map.get(params, :page_size, 200)
    order = Map.get(params, :order, :scrobbled_at)

    base_query = from(t in Track)

    search_query =
      if query == "" do
        base_query
      else
        query_term = "%#{String.downcase(query)}%"

        from t in base_query,
          where:
            like(fragment("lower(?)", t.title), ^query_term) or
              like(fragment("lower(json_extract(artist, '$.name'))"), ^query_term) or
              like(fragment("lower(json_extract(album, '$.title'))"), ^query_term)
      end

    ordered_query =
      case order do
        :scrobbled_at ->
          from t in search_query, order_by: [desc: t.scrobbled_at_uts]

        :title ->
          from t in search_query, order_by: [asc: t.title]

        :artist ->
          from t in search_query, order_by: [asc: fragment("json_extract(artist, '$.name')")]

        :album ->
          from t in search_query, order_by: [asc: fragment("json_extract(album, '$.title')")]
      end

    offset = (page - 1) * page_size

    from(t in ordered_query, limit: ^page_size, offset: ^offset)
    |> Repo.all()
  end

  @doc """
  Gets a single track by scrobbled_at_uts.
  """
  def get_track!(scrobbled_at_uts) when is_integer(scrobbled_at_uts) do
    Repo.get!(Track, scrobbled_at_uts)
  end

  def get_track!(scrobbled_at_uts) when is_binary(scrobbled_at_uts) do
    case Integer.parse(scrobbled_at_uts) do
      {id, ""} -> get_track!(id)
      _ -> raise Ecto.NoResultsError, queryable: Track
    end
  end

  @doc """
  Updates a track with the given attributes.
  """
  def update_track(%Track{} = track, attrs) do
    changeset = Track.changeset(track, attrs)
    Repo.update(changeset)
  end

  @doc """
  Deletes a track.
  """
  def delete_track(%Track{} = track) do
    Repo.delete(track)
  end

  @doc """
  Counts tracks matching the search query.
  """
  def search_tracks_count(query \\ "") do
    base_query = from(t in Track)

    search_query =
      if query == "" do
        base_query
      else
        query_term = "%#{String.downcase(query)}%"

        from t in base_query,
          where:
            like(fragment("lower(?)", t.title), ^query_term) or
              like(fragment("lower(json_extract(artist, '$.name'))"), ^query_term) or
              like(fragment("lower(json_extract(album, '$.title'))"), ^query_term)
      end

    Repo.aggregate(search_query, :count, :scrobbled_at_uts)
  end
end
