defmodule Mv.Config do
  @moduledoc """
  Configuration helper functions for the application.

  Provides centralized access to configuration values to avoid
  magic strings/atoms scattered throughout the codebase.
  """

  @doc """
  Returns whether SQL sandbox mode is enabled.

  SQL sandbox mode is typically enabled in test environments
  to allow concurrent database access in tests.

  ## Returns

    - `true` if SQL sandbox is enabled
    - `false` otherwise
  """
  @spec sql_sandbox?() :: boolean()
  def sql_sandbox? do
    Application.get_env(:mv, :sql_sandbox, false)
  end

  @doc """
  Returns the maximum file size for CSV imports in bytes.

  Reads the `max_file_size_mb` value from the CSV import configuration
  and converts it to bytes.

  ## Returns

    - Maximum file size in bytes (default: 10_485_760 bytes = 10 MB)

  ## Examples

      iex> Mv.Config.csv_import_max_file_size_bytes()
      10_485_760
  """
  @spec csv_import_max_file_size_bytes() :: non_neg_integer()
  def csv_import_max_file_size_bytes do
    max_file_size_mb = get_csv_import_config(:max_file_size_mb, 10)
    max_file_size_mb * 1024 * 1024
  end

  @doc """
  Returns the maximum number of rows allowed in CSV imports.

  Reads the `max_rows` value from the CSV import configuration.

  ## Returns

    - Maximum number of rows (default: 1000)

  ## Examples

      iex> Mv.Config.csv_import_max_rows()
      1000
  """
  @spec csv_import_max_rows() :: pos_integer()
  def csv_import_max_rows do
    get_csv_import_config(:max_rows, 1000)
  end

  @doc """
  Returns the maximum file size for CSV imports in megabytes.

  Reads the `max_file_size_mb` value from the CSV import configuration.

  ## Returns

    - Maximum file size in megabytes (default: 10)

  ## Examples

      iex> Mv.Config.csv_import_max_file_size_mb()
      10
  """
  @spec csv_import_max_file_size_mb() :: pos_integer()
  def csv_import_max_file_size_mb do
    get_csv_import_config(:max_file_size_mb, 10)
  end

  # Helper function to get CSV import config values
  defp get_csv_import_config(key, default) do
    Application.get_env(:mv, :csv_import, [])
    |> Keyword.get(key, default)
    |> parse_and_validate_integer(default)
  end

  # Parses and validates integer configuration values.
  #
  # Accepts:
  # - Integer values (passed through)
  # - String integers (e.g., "1000") - parsed to integer
  # - Invalid values (e.g., "abc", nil) - falls back to default
  #
  # Always clamps the result to a minimum of 1 to ensure positive values.
  #
  # Note: We don't log warnings for unparseable values because:
  # - These functions may be called frequently (e.g., on every request)
  # - Logging would create excessive log spam
  # - The fallback to default provides a safe behavior
  # - Configuration errors should be caught during deployment/testing
  defp parse_and_validate_integer(value, _default) when is_integer(value) do
    max(1, value)
  end

  defp parse_and_validate_integer(value, default) when is_binary(value) do
    case Integer.parse(value) do
      {int, _remainder} -> max(1, int)
      :error -> default
    end
  end

  defp parse_and_validate_integer(_value, default) do
    default
  end

  @doc """
  Returns the maximum number of rows allowed in PDF exports.

  Reads the `row_limit` value from the PDF export configuration.

  ## Returns

    - Maximum number of rows (default: 5000)

  ## Examples

      iex> Mv.Config.pdf_export_row_limit()
      5000
  """
  @spec pdf_export_row_limit() :: pos_integer()
  def pdf_export_row_limit do
    get_pdf_export_config(:row_limit, 5000)
  end

  # Helper function to get PDF export config values
  defp get_pdf_export_config(key, default) do
    Application.get_env(:mv, :pdf_export, [])
    |> Keyword.get(key, default)
    |> parse_and_validate_integer(default)
  end

  # ---------------------------------------------------------------------------
  # Vereinfacht accounting software integration
  # ENV variables take priority; fallback to Settings from database.
  # ---------------------------------------------------------------------------

  @doc """
  Returns the Vereinfacht API base URL.

  Reads from `VEREINFACHT_API_URL` env first, then from Settings.
  """
  @spec vereinfacht_api_url() :: String.t() | nil
  def vereinfacht_api_url do
    env_or_setting("VEREINFACHT_API_URL", :vereinfacht_api_url)
  end

  @doc """
  Returns the Vereinfacht API key (Bearer token).

  Reads from `VEREINFACHT_API_KEY` env first, then from Settings.
  """
  @spec vereinfacht_api_key() :: String.t() | nil
  def vereinfacht_api_key do
    env_or_setting("VEREINFACHT_API_KEY", :vereinfacht_api_key)
  end

  @doc """
  Returns the Vereinfacht club ID for multi-tenancy.

  Reads from `VEREINFACHT_CLUB_ID` env first, then from Settings.
  """
  @spec vereinfacht_club_id() :: String.t() | nil
  def vereinfacht_club_id do
    env_or_setting("VEREINFACHT_CLUB_ID", :vereinfacht_club_id)
  end

  @doc """
  Returns the Vereinfacht app base URL for contact view links (frontend, not API).

  Reads from `VEREINFACHT_APP_URL` env first, then from Settings.
  Used to build links like https://app.verein.visuel.dev/en/admin/finances/contacts/{id}.
  If not set, derived from API URL by replacing host \"api.\" with \"app.\" when possible.
  """
  @spec vereinfacht_app_url() :: String.t() | nil
  def vereinfacht_app_url do
    env_or_setting("VEREINFACHT_APP_URL", :vereinfacht_app_url) ||
      derive_app_url_from_api_url(vereinfacht_api_url())
  end

  defp derive_app_url_from_api_url(nil), do: nil

  defp derive_app_url_from_api_url(api_url) when is_binary(api_url) do
    api_url = String.trim(api_url)
    uri = URI.parse(api_url)
    host = uri.host || ""

    if String.starts_with?(host, "api.") do
      app_host = "app." <> String.slice(host, 4..-1//1)
      scheme = uri.scheme || "https"
      "#{scheme}://#{app_host}"
    else
      nil
    end
  end

  defp derive_app_url_from_api_url(_), do: nil

  @doc """
  Returns true if Vereinfacht is fully configured (URL, API key, and club ID all set).
  """
  @spec vereinfacht_configured?() :: boolean()
  def vereinfacht_configured? do
    present?(vereinfacht_api_url()) and present?(vereinfacht_api_key()) and
      present?(vereinfacht_club_id())
  end

  @doc """
  Returns true if any Vereinfacht ENV variable is set (used to show hint in Settings UI).
  """
  @spec vereinfacht_env_configured?() :: boolean()
  def vereinfacht_env_configured? do
    vereinfacht_api_url_env_set?() or vereinfacht_api_key_env_set?() or
      vereinfacht_club_id_env_set?()
  end

  @doc """
  Returns true if VEREINFACHT_API_URL is set (field is read-only in Settings).
  """
  def vereinfacht_api_url_env_set?, do: env_set?("VEREINFACHT_API_URL")

  @doc """
  Returns true if VEREINFACHT_API_KEY is set (field is read-only in Settings).
  """
  def vereinfacht_api_key_env_set?, do: env_set?("VEREINFACHT_API_KEY")

  @doc """
  Returns true if VEREINFACHT_CLUB_ID is set (field is read-only in Settings).
  """
  def vereinfacht_club_id_env_set?, do: env_set?("VEREINFACHT_CLUB_ID")

  @doc """
  Returns true if VEREINFACHT_APP_URL is set (field is read-only in Settings).
  """
  def vereinfacht_app_url_env_set?, do: env_set?("VEREINFACHT_APP_URL")

  defp env_set?(key) do
    case System.get_env(key) do
      nil -> false
      v when is_binary(v) -> String.trim(v) != ""
      _ -> false
    end
  end

  defp env_or_setting(env_key, setting_key) do
    case System.get_env(env_key) do
      nil -> get_vereinfacht_from_settings(setting_key)
      value -> trim_nil(value)
    end
  end

  defp env_or_setting_bool(env_key, setting_key) do
    case System.get_env(env_key) do
      nil ->
        get_from_settings_bool(setting_key)

      value when is_binary(value) ->
        v = String.trim(value) |> String.downcase()
        v in ["true", "1", "yes"]

      _ ->
        false
    end
  end

  defp get_vereinfacht_from_settings(key) do
    get_from_settings(key)
  end

  defp get_from_settings(key) do
    case Mv.Membership.get_settings() do
      {:ok, settings} -> settings |> Map.get(key) |> trim_nil()
      {:error, _} -> nil
    end
  end

  defp get_from_settings_bool(key) do
    case Mv.Membership.get_settings() do
      {:ok, settings} ->
        case Map.get(settings, key) do
          true -> true
          _ -> false
        end

      {:error, _} ->
        false
    end
  end

  defp trim_nil(nil), do: nil

  defp trim_nil(s) when is_binary(s) do
    t = String.trim(s)
    if t == "", do: nil, else: t
  end

  @doc """
  Returns the URL to view a finance contact in the Vereinfacht app (frontend).

  Uses the configured app base URL (or derived from API URL) and appends
  /en/admin/finances/contacts/{id}. Returns nil if no app URL can be determined.
  """
  @spec vereinfacht_contact_view_url(String.t()) :: String.t() | nil
  def vereinfacht_contact_view_url(contact_id) when is_binary(contact_id) do
    base = vereinfacht_app_url()

    if present?(base) do
      base
      |> String.trim_trailing("/")
      |> then(&"#{&1}/en/admin/finances/contacts/#{contact_id}")
    else
      nil
    end
  end

  defp present?(nil), do: false
  defp present?(s) when is_binary(s), do: String.trim(s) != ""
  defp present?(_), do: false

  # ---------------------------------------------------------------------------
  # OIDC authentication
  # ENV variables take priority; fallback to Settings from database.
  # ---------------------------------------------------------------------------

  @doc """
  Returns the OIDC client ID. ENV first, then Settings.
  """
  @spec oidc_client_id() :: String.t() | nil
  def oidc_client_id do
    env_or_setting("OIDC_CLIENT_ID", :oidc_client_id)
  end

  @doc """
  Returns the OIDC provider base URL. ENV first, then Settings.
  """
  @spec oidc_base_url() :: String.t() | nil
  def oidc_base_url do
    env_or_setting("OIDC_BASE_URL", :oidc_base_url)
  end

  @doc """
  Returns the OIDC redirect URI. ENV first, then Settings.
  """
  @spec oidc_redirect_uri() :: String.t() | nil
  def oidc_redirect_uri do
    env_or_setting("OIDC_REDIRECT_URI", :oidc_redirect_uri)
  end

  @doc """
  Returns the OIDC client secret. ENV first, then Settings.
  """
  @spec oidc_client_secret() :: String.t() | nil
  def oidc_client_secret do
    env_or_setting("OIDC_CLIENT_SECRET", :oidc_client_secret)
  end

  @doc """
  Returns the OIDC admin group name (for role sync). ENV first, then Settings.
  """
  @spec oidc_admin_group_name() :: String.t() | nil
  def oidc_admin_group_name do
    env_or_setting("OIDC_ADMIN_GROUP_NAME", :oidc_admin_group_name)
  end

  @doc """
  Returns the OIDC groups claim name (default "groups"). ENV first, then Settings.
  """
  @spec oidc_groups_claim() :: String.t() | nil
  def oidc_groups_claim do
    case env_or_setting("OIDC_GROUPS_CLAIM", :oidc_groups_claim) do
      nil -> "groups"
      v -> v
    end
  end

  @doc """
  Returns true if any OIDC ENV variable is set (used to show hint in Settings UI).
  """
  @spec oidc_env_configured?() :: boolean()
  def oidc_env_configured? do
    oidc_client_id_env_set?() or oidc_base_url_env_set?() or
      oidc_redirect_uri_env_set?() or oidc_client_secret_env_set?() or
      oidc_admin_group_name_env_set?() or oidc_groups_claim_env_set?() or
      oidc_only_env_set?()
  end

  @doc """
  Returns true when OIDC is configured and can be used for sign-in (client ID, base URL,
  redirect URI, and client secret must be set). Used to show or hide the Single Sign-On button on the
  sign-in page. Without client secret, the OIDC flow fails with MissingSecret; without redirect_uri,
  the OIDC Plug crashes with URI.new(nil).
  """
  @spec oidc_configured?() :: boolean()
  def oidc_configured? do
    id = oidc_client_id()
    base = oidc_base_url()
    secret = oidc_client_secret()
    redirect = oidc_redirect_uri()
    present = &(is_binary(&1) and String.trim(&1) != "")
    present.(id) and present.(base) and present.(secret) and present.(redirect)
  end

  @doc """
  Returns true when only OIDC sign-in should be shown (password login hidden).
  ENV OIDC_ONLY first (true/1/yes vs false/0/no), then Settings.oidc_only.
  Only has effect when OIDC is configured; when false or OIDC not configured, both password and OIDC are shown as usual.
  """
  @spec oidc_only?() :: boolean()
  def oidc_only? do
    env_or_setting_bool("OIDC_ONLY", :oidc_only)
  end

  def oidc_client_id_env_set?, do: env_set?("OIDC_CLIENT_ID")
  def oidc_base_url_env_set?, do: env_set?("OIDC_BASE_URL")
  def oidc_redirect_uri_env_set?, do: env_set?("OIDC_REDIRECT_URI")
  def oidc_client_secret_env_set?, do: env_set?("OIDC_CLIENT_SECRET")
  def oidc_admin_group_name_env_set?, do: env_set?("OIDC_ADMIN_GROUP_NAME")
  def oidc_groups_claim_env_set?, do: env_set?("OIDC_GROUPS_CLAIM")
  def oidc_only_env_set?, do: env_set?("OIDC_ONLY")
end
