defmodule Aprsme.Callsign do
  @moduledoc """
  Validation and normalization for APRS transport-safe identifiers.

  ## Terminology

  This module validates **transport-safe APRS identifiers**, not strict amateur
  radio AX.25 callsigns. The distinction is intentional:

  - **AX.25 callsigns** are short (3-6 chars), format-restricted (e.g. `K5ABC`),
    and used on the RF side by amateur radio operators. The `User` schema uses
    this stricter format for registered user callsigns.

  - **Transport-safe identifiers** accept a broader character set
    (`[A-Z0-9]+` segments separated by hyphens, up to 20 bytes). This includes
    tactical callsigns, object names, gateway identifiers, and other
    APRS-IS originated labels that appear in the `sender`/`destination` fields
    of packets on the APRS internet stream.

  This module purposefully does **not** enforce AX.25 format constraints —
  tightening it would reject valid APRS-IS traffic. Future changes to
  validation rules should remain additive (rejecting more at the edges) rather
  than narrowing the accepted format.

  See also: `AprsmeWeb.Live.Shared.ParamUtils.valid_callsign?/1` for
  user-facing search validation, and `AprsmeWeb.Api.V1.CallsignController`
  for API endpoint validation.
  """

  @safe_identifier_regex ~r/^[A-Z0-9]+(?:-[A-Z0-9]+)*$/

  @spec valid?(term()) :: boolean()
  def valid?(callsign) when is_binary(callsign) do
    normalized = normalize(callsign)
    byte_size(normalized) in 1..20 and Regex.match?(@safe_identifier_regex, normalized)
  end

  def valid?(_callsign), do: false

  @spec normalize(term()) :: String.t()
  def normalize(callsign) when is_binary(callsign), do: callsign |> String.trim() |> String.upcase()
  def normalize(_callsign), do: ""

  @spec matches?(term(), term()) :: boolean()
  def matches?(left, right), do: normalize(left) == normalize(right)

  @spec extract_base(term()) :: String.t()
  def extract_base(callsign), do: callsign |> extract_parts() |> elem(0)

  @spec extract_ssid(term()) :: String.t()
  def extract_ssid(callsign), do: callsign |> extract_parts() |> elem(1)

  @spec extract_parts(term()) :: {String.t(), String.t()}
  def extract_parts(callsign) when is_binary(callsign) do
    normalized = normalize(callsign)

    case Regex.run(~r/^(.*)-([0-9]+)$/, normalized) do
      [_, base, ssid] -> {base, ssid}
      _ -> {normalized, "0"}
    end
  end

  def extract_parts(_callsign), do: {"", "0"}
end
