defmodule ErrorTracker.ErrorNotifier do
  @moduledoc false

  use GenServer
  require Logger

  alias ErrorTracker.ErrorNotifier.Email

  @cleanup_interval :timer.minutes(5)

  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @impl true
  def init(_opts) do
    if has_valid_config?() do
      attach_telemetry()
      schedule_cleanup()
      {:ok, %{errors: %{}}}
    else
      :ignore
    end
  end

  @impl true
  def handle_info({:telemetry_event, event_name, _measurements, metadata}, state) do
    case event_name do
      [:error_tracker, :error, :new] ->
        reason = truncate_reason(metadata.occurrence.reason)

        {_result, new_state} =
          maybe_notify(metadata.occurrence, false, "New Error! (#{reason})", state)

        {:noreply, new_state}

      [:error_tracker, :occurrence, :new] ->
        reason = truncate_reason(metadata.occurrence.reason)

        {_result, new_state} =
          maybe_notify(metadata.occurrence, metadata.muted, "Error: #{reason}", state)

        {:noreply, new_state}

      _ ->
        {:noreply, state}
    end
  end

  def handle_info(:cleanup, state) do
    schedule_cleanup()
    {:noreply, cleanup_old_records(state)}
  end

  def handle_info(_msg, state), do: {:noreply, state}

  @doc false
  def handle_telemetry_event(event_name, measurements, metadata, _config) do
    case Process.whereis(__MODULE__) do
      nil -> :ok
      pid -> send(pid, {:telemetry_event, event_name, measurements, metadata})
    end
  end

  # -- Private --

  defp attach_telemetry do
    events = [
      [:error_tracker, :error, :new],
      [:error_tracker, :occurrence, :new]
    ]

    :telemetry.detach("error-tracker-notifications")

    :telemetry.attach_many(
      "error-tracker-notifications",
      events,
      &__MODULE__.handle_telemetry_event/4,
      nil
    )
  end

  defp maybe_notify(occurrence, true, _header, state) do
    Logger.debug("Skipping notification for muted error #{occurrence.error_id}")
    {:skipped, state}
  end

  defp maybe_notify(occurrence, _muted, header, state) do
    error_id = occurrence.error_id
    now = System.system_time(:second)
    throttle_seconds = config()[:throttle_seconds] || 10

    error_state = Map.get(state.errors, error_id, %{count: 0, last_time: 0})
    time_since_last = now - error_state.last_time

    if error_state.last_time == 0 || time_since_last >= throttle_seconds do
      header_with_count = format_header_with_count(header, error_state.count)
      result = Email.send(occurrence, header_with_count)
      updated_errors = Map.put(state.errors, error_id, %{count: 0, last_time: now})
      {result, %{state | errors: updated_errors}}
    else
      updated_error = %{count: error_state.count + 1, last_time: error_state.last_time}
      updated_errors = Map.put(state.errors, error_id, updated_error)

      Logger.debug("Throttled notification for error #{error_id}. Count: #{updated_error.count}")

      {:throttled, %{state | errors: updated_errors}}
    end
  end

  defp cleanup_old_records(state) do
    now = System.system_time(:second)
    one_hour_ago = now - 3600

    cleaned_errors =
      state.errors
      |> Enum.filter(fn {_error_id, %{last_time: last_time}} -> last_time >= one_hour_ago end)
      |> Map.new()

    %{state | errors: cleaned_errors}
  end

  defp format_header_with_count(header, count) when count > 1,
    do: "#{header} (#{count} occurrences)"

  defp format_header_with_count(header, _count), do: header

  defp schedule_cleanup do
    Process.send_after(self(), :cleanup, @cleanup_interval)
  end

  defp truncate_reason(reason) when is_binary(reason) do
    if String.length(reason) > 80, do: String.slice(reason, 0, 77) <> "...", else: reason
  end

  defp truncate_reason(reason), do: reason |> inspect() |> truncate_reason()

  defp has_valid_config? do
    conf = config()
    not is_nil(conf[:from_email]) and not is_nil(conf[:to_email]) and not is_nil(conf[:mailer])
  end

  defp config do
    Application.get_env(:music_library, __MODULE__, [])
  end
end
