defmodule MvWeb.MemberPdfExportController do
  @moduledoc """
  PDF export for members.

  Expects `payload` as JSON string form param.
  Uses the same actor/permissions as the member overview.
  """

  use MvWeb, :controller

  require Logger

  alias Mv.Authorization.Actor
  alias Mv.Membership.{MemberExport, MemberExport.Build, MembersPDF}
  alias MvWeb.Translations.MemberFields

  use Gettext, backend: MvWeb.Gettext

  @payload_required_message "payload required"
  @invalid_json_message "invalid JSON"
  @export_failed_message "Failed to generate PDF export"

  @allowed_member_field_strings Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)

  def export(conn, %{"payload" => payload}) when is_binary(payload) do
    actor = current_actor(conn)

    if is_nil(actor) do
      forbidden(conn)
    else
      locale = get_locale(conn)
      club_name = get_club_name()

      with {:ok, decoded} <- decode_json_map(payload),
           parsed <- MemberExport.parse_params(decoded),
           {:ok, export_data} <- Build.build(actor, parsed, &label_for_column/1),
           {:ok, pdf_binary} <-
             MembersPDF.render(export_data, locale: locale, club_name: club_name) do
        filename = "members-#{Date.utc_today()}.pdf"

        send_download(
          conn,
          {:binary, pdf_binary},
          filename: filename,
          content_type: "application/pdf"
        )
      else
        {:error, :invalid_json} ->
          bad_request(conn, @invalid_json_message)

        {:error, :forbidden} ->
          forbidden(conn)

        {:error, {:row_limit_exceeded, row_count, max_rows}} ->
          unprocessable_entity(conn, %{
            error: "row_limit_exceeded",
            message:
              gettext("Export contains %{count} rows, maximum is %{max}",
                count: row_count,
                max: max_rows
              ),
            row_count: row_count,
            max_rows: max_rows
          })

        {:error, reason} ->
          Logger.warning("PDF export failed: #{inspect(reason)}")

          internal_error(conn, %{
            error: "export_failed",
            message: gettext(@export_failed_message)
          })
      end
    end
  end

  def export(conn, _params) do
    bad_request(conn, @payload_required_message)
  end

  # --- Actor / auth ---

  defp current_actor(conn) do
    conn.assigns[:current_user]
    |> Actor.ensure_loaded()
  end

  defp forbidden(conn) do
    conn
    |> put_status(:forbidden)
    |> json(%{error: "forbidden", message: "Forbidden"})
    |> halt()
  end

  # --- Decoding / validation ---

  defp decode_json_map(payload) when is_binary(payload) do
    case Jason.decode(payload) do
      {:ok, decoded} when is_map(decoded) -> {:ok, decoded}
      _ -> {:error, :invalid_json}
    end
  end

  # --- Column labels ---

  # Goal: translate known member fields to UI labels, but never crash.
  # - Atoms: label directly.
  # - Binaries: only translate if they are known member fields (allowlist); otherwise return the string.
  # This avoids String.to_existing_atom/1 exceptions for arbitrary keys (e.g., "custom_field_...").
  defp label_for_column(key) when is_atom(key) do
    MemberFields.label(key)
  end

  defp label_for_column(key) when is_binary(key) do
    if key in @allowed_member_field_strings do
      # Safe because key is in allowlist which originates from existing atoms
      MemberFields.label(String.to_existing_atom(key))
    else
      key
    end
  end

  defp label_for_column(key) do
    to_string(key)
  end

  # --- Locale and club name ---

  defp get_locale(conn) do
    conn.assigns[:locale] || Gettext.get_locale(MvWeb.Gettext) || "en"
  end

  defp get_club_name do
    case Mv.Membership.get_settings() do
      {:ok, settings} -> settings.club_name
      _ -> "Club"
    end
  end

  # --- JSON responses ---

  defp bad_request(conn, message) when is_binary(message) do
    conn
    |> put_status(:bad_request)
    |> json(%{error: "bad_request", message: message})
  end

  defp unprocessable_entity(conn, body) when is_map(body) do
    conn
    |> put_status(:unprocessable_entity)
    |> json(body)
  end

  defp internal_error(conn, body) when is_map(body) do
    conn
    |> put_status(:internal_server_error)
    |> json(body)
  end
end
