defmodule AprsmeWeb.Live.Shared.ParamUtils do
  @moduledoc """
  Shared parameter parsing, validation, and sanitization utilities.
  Used across multiple LiveView modules for consistent parameter handling.
  """

  @doc """
  Parse float value within specified range with sanitization.
  """
  @spec parse_float_in_range(binary() | any(), float(), float(), float()) :: float()
  def parse_float_in_range(str, default, min, max) when is_binary(str) do
    parse_in_range(str, default, min, max, &Float.parse/1, fn v -> finite?(v) && v end)
  end

  def parse_float_in_range(_, default, _, _), do: default

  @doc """
  Parse integer value within specified range with sanitization.
  """
  @spec parse_int_in_range(binary() | any(), integer(), integer(), integer()) :: integer()
  def parse_int_in_range(str, default, min, max) when is_binary(str) do
    parse_in_range(str, default, min, max, &Integer.parse/1)
  end

  def parse_int_in_range(_, default, _, _), do: default

  defp parse_in_range(str, default, min, max, parser, validator \\ fn v -> v end) do
    sanitized = sanitize_numeric_string(str)

    case parser.(sanitized) do
      {val, _} when val >= min and val <= max ->
        validator.(val) || default

      _ ->
        default
    end
  end

  @doc """
  Sanitize numeric strings to prevent injection attacks.
  """
  @spec sanitize_numeric_string(binary() | any()) :: binary()
  def sanitize_numeric_string(str) when is_binary(str) do
    # Remove any potentially dangerous characters, keeping only numbers, dots, minus, and e/E for scientific notation
    str
    |> String.trim()
    |> String.replace(~r/[^\d\.\-eE]/, "")
    # Prevent extremely long inputs
    |> limit_string_length(20)
  end

  def sanitize_numeric_string(_), do: ""

  @doc """
  Limit string length to prevent DoS attacks.
  """
  @spec limit_string_length(binary(), integer()) :: binary()
  def limit_string_length(str, max_length) when byte_size(str) > max_length do
    :binary.part(str, 0, max_length)
  end

  def limit_string_length(str, _), do: str

  @doc """
  Check if a float is finite (not infinity or NaN).
  """
  @spec finite?(float() | any()) :: boolean()
  def finite?(float) when is_float(float) do
    # In Elixir, we can't have infinity or NaN in regular floats
    # This function is kept for API compatibility
    true
  end

  def finite?(_), do: false

  @doc """
  Safely parse and clamp coordinate values.
  """
  @spec safe_parse_coordinate(binary() | number() | any(), float(), float(), float()) :: float()
  def safe_parse_coordinate(value, default, min, max) when is_binary(value) do
    case parse_float_in_range(value, default, min, max) do
      ^default -> default
      parsed -> clamp_coordinate(parsed, min, max)
    end
  end

  def safe_parse_coordinate(value, _default, min, max) when is_number(value) do
    clamp_coordinate(value, min, max)
  end

  def safe_parse_coordinate(_, default, _, _), do: default

  @doc """
  Clamp coordinate value within bounds.
  """
  @spec clamp_coordinate(number(), number(), number()) :: float()
  def clamp_coordinate(value, min, max) when is_number(value) do
    value |> max(min) |> min(max)
  end

  def clamp_coordinate(_, _, _), do: 0.0

  @doc """
  Clamp zoom level within valid range.
  """
  @spec clamp_zoom(binary() | integer() | float() | any()) :: integer()
  def clamp_zoom(zoom) when is_binary(zoom) do
    parse_int_in_range(zoom, 5, 1, 20)
  end

  def clamp_zoom(zoom) when is_integer(zoom) do
    max(1, min(20, zoom))
  end

  def clamp_zoom(zoom) when is_float(zoom) do
    max(1, min(20, trunc(zoom)))
  end

  def clamp_zoom(_), do: 5

  @doc """
  Check if a number is finite.
  """
  @spec finite_number?(number() | any()) :: boolean()
  def finite_number?(num) when is_number(num) do
    # Convert integer to float for check
    finite?(num / 1.0)
  end

  def finite_number?(_), do: false

  @doc """
  Validate callsign format.
  """
  @spec valid_callsign?(binary()) :: boolean()
  def valid_callsign?(callsign) when is_binary(callsign) do
    # Basic callsign validation - alphanumeric with optional dash and suffix
    String.match?(callsign, ~r/^[A-Z0-9]{1,6}(-[A-Z0-9]{1,2})?$/i)
  end

  def valid_callsign?(_), do: false

  @doc """
  Sanitize path string for RF path visualization.
  """
  @spec sanitize_path_string(binary() | any()) :: binary()
  def sanitize_path_string(path) when is_binary(path) do
    # Remove potentially dangerous characters, keep only alphanumeric, dash, comma, space, asterisk
    path
    |> String.trim()
    |> String.replace(~r/[^A-Za-z0-9\-,\s\*]/, "")
    |> limit_string_length(200)
  end

  def sanitize_path_string(_), do: ""

  @doc """
  Converts various numeric types to float for consistent display.
  """
  @spec to_float(any()) :: float()
  def to_float(value) do
    Aprsme.EncodingUtils.to_float(value) || 0.0
  end
end
