Skip to main content

lib/phoenix_live_calendar/utils/safe.ex

defmodule PhoenixLiveCalendar.Utils.Safe do
  @moduledoc false
  # Internal helpers for defensive programming.
  # Prevents crashes from bad input data by providing safe fallbacks.

  require Logger

  @doc """
  Safely converts a value to a Date, returning nil on failure.
  """
  def to_date(%Date{} = d), do: d
  def to_date(%DateTime{} = dt), do: DateTime.to_date(dt)
  def to_date(%NaiveDateTime{} = ndt), do: NaiveDateTime.to_date(ndt)

  def to_date(str) when is_binary(str) do
    case Date.from_iso8601(str) do
      {:ok, date} -> date
      _ -> nil
    end
  end

  def to_date(other) do
    Logger.warning("[PhoenixLiveCalendar] Cannot convert to Date: #{inspect(other)}")
    nil
  end

  @doc """
  Safely converts a value to a Time, returning nil on failure.
  """
  def to_time(%Time{} = t), do: t
  def to_time(%DateTime{} = dt), do: DateTime.to_time(dt)
  def to_time(%NaiveDateTime{} = ndt), do: NaiveDateTime.to_time(ndt)

  def to_time(str) when is_binary(str) do
    case Time.from_iso8601(str) do
      {:ok, time} -> time
      _ -> nil
    end
  end

  def to_time(other) do
    Logger.warning("[PhoenixLiveCalendar] Cannot convert to Time: #{inspect(other)}")
    nil
  end

  @doc """
  Wraps a function call in a try/rescue, returning the fallback on any error.
  Logs the error at warning level.
  """
  def safe_call(fun, fallback \\ nil) do
    fun.()
  rescue
    e ->
      Logger.warning("[PhoenixLiveCalendar] Error: #{Exception.message(e)}")
      fallback
  end

  @doc """
  Ensures a value is a list, wrapping non-list values.
  """
  def ensure_list(nil), do: []
  def ensure_list(list) when is_list(list), do: list

  def ensure_list(other) do
    Logger.warning("[PhoenixLiveCalendar] Expected list, got: #{inspect(other)}")
    []
  end

  @doc """
  Ensures a value is a non-negative integer, returning default on failure.
  """
  def ensure_pos_integer(val, _default) when is_integer(val) and val > 0, do: val

  def ensure_pos_integer(val, default) do
    Logger.warning(
      "[PhoenixLiveCalendar] Expected positive integer, got: #{inspect(val)}, using #{default}"
    )

    default
  end

  @doc """
  Validates a CSS dimension value (e.g., "3rem", "48px", "50%", "5vh").
  Returns the value if safe, or a fallback if not.
  """
  def sanitize_css_dimension(value, fallback \\ "3rem")

  def sanitize_css_dimension(value, _fallback) when is_binary(value) do
    if Regex.match?(~r/^\d+(\.\d+)?\s*(px|rem|em|vh|vw|%|ch|ex|vmin|vmax)$/, value) do
      value
    else
      Logger.warning(
        "[PhoenixLiveCalendar] Invalid CSS dimension: #{inspect(value)}, using fallback"
      )

      "3rem"
    end
  end

  def sanitize_css_dimension(_, fallback), do: fallback

  @doc """
  Safely filters events, skipping any that would cause errors.
  """
  def safe_filter_events(events) when is_list(events) do
    Enum.filter(events, fn
      %PhoenixLiveCalendar.Event{id: id, start: start} when not is_nil(id) and not is_nil(start) ->
        true

      invalid ->
        Logger.warning("[PhoenixLiveCalendar] Skipping invalid event: #{inspect(invalid)}")
        false
    end)
  end

  def safe_filter_events(other) do
    Logger.warning("[PhoenixLiveCalendar] Expected event list, got: #{inspect(other)}")
    []
  end

  @doc """
  Infers the daisyUI text content color from a background color class.

  "bg-warning" -> "text-warning-content", "bg-primary/80" -> "text-primary-content", etc.
  Falls back to "text-base-content" for unknown patterns, "text-primary-content" for nil.
  """
  def infer_text_color(nil), do: "text-primary-content"

  def infer_text_color(bg_class) when is_binary(bg_class) do
    case Regex.run(~r/bg-(primary|secondary|accent|neutral|info|success|warning|error)/, bg_class) do
      [_, color] -> "text-#{color}-content"
      _ -> "text-base-content"
    end
  end
end