defmodule LastFm.Refresh do
  @moduledoc """
  A GenServer that manages periodic refreshing of Last.fm scrobbled tracks.

  This module is responsible for:
  - Fetching recent tracks from Last.fm at configurable intervals
  - Updating an in-memory feed with the latest tracks
  - Supporting both automatic and manual refresh modes

  ## Configuration

  The server accepts a `LastFm.Config` struct with the following options:
  - `auto_refresh`: When true, automatically starts refreshing on init
  - `refresh_interval`: Time in milliseconds between refresh attempts

  ## Operation Modes

  1. Automatic Mode (`auto_refresh: true`):
     - Starts refreshing immediately on initialization
     - Continues to refresh at the configured interval
     - Handles failures gracefully by continuing to retry

  2. Manual Mode (`auto_refresh: false`):
     - Server remains dormant on initialization
     - Refreshes only occur via explicit `refresh/0` calls
     - Useful for testing or controlled refresh scenarios

  ## Usage

      # Manual refresh
      LastFm.Refresh.refresh()

  The module uses `LastFm.Feed` to store and broadcast track updates to subscribers.
  """

  use GenServer

  alias LastFm.{API, Config, Feed}

  require Logger

  @type config :: Config.t()

  @spec start_link(config) :: GenServer.on_start()
  def start_link(config) do
    GenServer.start_link(__MODULE__, config, name: __MODULE__)
  end

  @spec refresh() :: :ok
  def refresh do
    GenServer.call(__MODULE__, :refresh, 10_000)
  end

  @impl true
  @spec init(config) :: {:ok, config, {:continue, :refresh}} | :ignore
  def init(config) do
    if config.auto_refresh do
      {:ok, config, {:continue, :refresh}}
    else
      :ignore
    end
  end

  @impl true
  @spec handle_call(:refresh, GenServer.from(), config) ::
          {:reply, :ok | {:error, term()}, config, pos_integer()}
  def handle_call(:refresh, _from, config) do
    case API.get_recent_tracks(config) do
      {:ok, tracks} ->
        Feed.update(tracks)
        {:reply, :ok, config, config.refresh_interval}

      error ->
        # TODO: think about failure scenario - error is logged at the API level
        {:reply, error, config, config.refresh_interval}
    end
  end

  @impl true
  @spec handle_continue(atom(), config) :: {:noreply, config, pos_integer()}
  def handle_continue(:refresh, config), do: refresh(config)

  @impl true
  @spec handle_info(atom(), config) :: {:noreply, config, pos_integer()}
  def handle_info(:refresh, config), do: refresh(config)

  def handle_info(:timeout, config), do: refresh(config)

  defp refresh(config) do
    case API.get_recent_tracks(config) do
      {:ok, tracks} ->
        Feed.update(tracks)
        {:noreply, config, config.refresh_interval}

      {:error, error} ->
        if API.ErrorResponse.retryable_error?(error) do
          {:noreply, config, API.ErrorResponse.retry_delay(error)}
        else
          {:stop, error, config}
        end
    end
  end
end
