defmodule Towerops.HoneybadgerNoticeFilter do
  @moduledoc """
  Honeybadger notice filter that emails a raw stacktrace to the
  ops contact whenever an exception is reported.

  Implements `Honeybadger.NoticeFilter` — receives the fully-built
  notice, sends an email, and returns the notice unchanged so it
  still reaches Honeybadger.
  """

  @behaviour Honeybadger.NoticeFilter

  import Swoosh.Email

  alias Towerops.Mailer

  require Logger

  @recipient "graham@mcintire.me"

  @impl Honeybadger.NoticeFilter
  def filter(%Honeybadger.Notice{} = notice) do
    if shutdown_error?(notice) do
      nil
    else
      send_error_email(notice)
      notice
    end
  end

  defp shutdown_error?(%Honeybadger.Notice{error: error}) do
    msg = error.message || ""

    String.contains?(msg, "port_died") or
      (String.contains?(msg, "write_failed") and String.contains?(msg, "epipe")) or
      String.contains?(msg, "broken pipe (epipe)")
  end

  defp send_error_email(%Honeybadger.Notice{error: error, server: server}) do
    from_address = Application.get_env(:towerops, :mailer_from, {"Towerops", "hi@towerops.net"})
    subject = "[#{server.environment_name}] #{error.class}: #{truncate(error.message, 120)}"

    body = format_body(error, server)

    email =
      new()
      |> to(@recipient)
      |> from(from_address)
      |> subject(subject)
      |> text_body(body)

    case Mailer.deliver(email) do
      {:ok, _} ->
        :ok

      {:error, reason} ->
        Logger.error("Failed to send Honeybadger error email: #{inspect(reason)}")
        :error
    end
  end

  defp format_body(error, server) do
    backtrace = format_backtrace(error.backtrace)

    """
    Exception: #{error.class}
    Message: #{error.message}

    Environment: #{server.environment_name}
    Hostname: #{server.hostname}

    Stacktrace:
    #{backtrace}
    """
  end

  defp format_backtrace(backtrace) when is_list(backtrace) do
    Enum.map_join(backtrace, "\n", fn entry ->
      "  #{entry.file}:#{entry.number} in #{entry.method}"
    end)
  end

  defp format_backtrace(_), do: "  (no stacktrace available)"

  defp truncate(nil, _max), do: "(no message)"

  defp truncate(message, max) when byte_size(message) > max do
    String.slice(message, 0, max) <> "..."
  end

  defp truncate(message, _max), do: message
end
