defmodule PhoenixAnalytics.Services.Batcher do
  @moduledoc """
  A GenServer module for batching and inserting RequestLog entries into the database.

  This module provides functionality to efficiently insert RequestLog entries by batching them
  and inserting them in bulk. It uses a GenServer to manage the state of the batch and
  periodically flush the batch to the database.

  The batch is processed and inserted into the database either when it reaches 1_000 logs
  or after 1 second, whichever comes first.

  ## Usage

  To insert a RequestLog:

      PhoenixAnalytics.Services.Batcher.insert(%PhoenixAnalytics.Entities.RequestLog{})

  Alternatively, you can send an event to PubSub for request_log insertion:

      PhoenixAnalytics.Services.PubSub.broadcast(:request_sent, %PhoenixAnalytics.Entities.RequestLog{})

  The module will handle batching and inserting the logs automatically.
  This approach allows for handling distributed app scenarios.
  """

  use GenServer

  alias PhoenixAnalytics.Services.PubSub
  alias PhoenixAnalytics.Repo

  @batch_size 1_000
  @timeout 1_000

  # --- client callbacks ---

  @doc false
  def start_link(_) do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end

  @doc """
  Inserts a RequestLog into the batch queue.
  The batch is processed and inserted into the database either when it reaches 1_000 logs or after 1 second, whichever comes first.

  ## Parameters

    - request_log: A PhoenixAnalytics.Entities.RequestLog struct to be inserted.

  ## Examples

      iex> PhoenixAnalytics.Services.Batcher.insert(%PhoenixAnalytics.Entities.RequestLog{})

  Note: You can also use PubSub to insert a RequestLog, which is useful for distributed app scenarios:

      iex> PhoenixAnalytics.Services.PubSub.broadcast(:request_sent, %PhoenixAnalytics.Entities.RequestLog{})

  """
  def insert(request_log) do
    GenServer.cast(__MODULE__, {:insert, request_log})
  end

  # --- server callbacks ---

  @doc false
  @impl true
  def init(_state) do
    PubSub.subscribe()

    :timer.send_interval(@timeout, :check_batch)
    {:ok, %{batch: [], last_insert_time: :os.system_time(:millisecond)}}
  end

  @doc false
  @impl true
  def handle_cast({:insert, request_log}, state) do
    new_batch = [request_log | state.batch]

    if length(new_batch) >= @batch_size do
      send_batch(new_batch)

      {:noreply, %{state | batch: [], last_insert_time: :os.system_time(:millisecond)}}
    else
      {:noreply, %{state | batch: new_batch}}
    end
  end

  @doc false
  @impl true
  def handle_info({:request_sent, request_log}, state) do
    GenServer.cast(__MODULE__, {:insert, request_log})
    {:noreply, state}
  end

  @doc false
  @impl true
  def handle_info(:check_batch, state) do
    current_time = :os.system_time(:millisecond)
    time_diff = current_time - state.last_insert_time

    if time_diff >= @timeout && length(state.batch) > 0 do
      send_batch(state.batch)
      {:noreply, %{state | batch: [], last_insert_time: current_time}}
    else
      {:noreply, state}
    end
  end

  @doc """
  Sends a batch of RequestLogs to be inserted into the database.

  ## Parameters

    - batch: A list of PhoenixAnalytics.Entities.RequestLog structs to be inserted.

  ## Examples

      iex> PhoenixAnalytics.Services.Batcher.send_batch([%PhoenixAnalytics.Entities.RequestLog{}, %PhoenixAnalytics.Entities.RequestLog{}])

  """
  def send_batch([]), do: []
  def send_batch(batch), do: Repo.insert_many(batch)
end
