defmodule Aprsme.PacketSanitizer do
  @moduledoc """
  Sanitizes packet data to ensure it fits within database constraints.
  This module provides truncation for long strings to prevent database errors.
  """

  # Define max lengths for fields that might still have constraints
  # Even though we're migrating to text, this provides safety
  @max_lengths %{
    # Keep reasonable limits for key fields
    base_callsign: 20,
    sender: 20,
    destination: 20,
    ssid: 10,
    data_type: 50,
    symbol_code: 5,
    symbol_table_id: 5,
    region: 50,
    timestamp: 50,
    message_number: 20,
    addressee: 50,

    # These fields can be longer but still have sanity limits
    path: 500,
    manufacturer: 100,
    equipment_type: 100,
    device_identifier: 255,
    item_name: 100,
    object_name: 100,

    # Very long fields
    raw_packet: 5000,
    comment: 2000,
    message_text: 2000
  }

  # Max lengths for string values inside the JSONB `data` map
  @data_string_max_lengths %{
    "information_field" => 5000,
    "radiorange" => 1000,
    "telemetry_bits" => 1000,
    "format" => 100
  }

  @doc """
  Sanitizes a packet map by truncating string fields that exceed maximum lengths.
  """
  @spec sanitize_packet(map()) :: map()
  def sanitize_packet(packet) when is_map(packet) do
    Map.new(packet, fn {key, value} -> {key, sanitize_field(key, value)} end)
  end

  defp sanitize_field(:data, value) when is_map(value) do
    sanitize_data_map(value)
  end

  defp sanitize_field(key, value) when is_binary(value) do
    case Map.get(@max_lengths, key) do
      nil ->
        # No limit defined, return as-is
        value

      max_length ->
        truncate_string(value, max_length)
    end
  end

  defp sanitize_field(_key, value), do: value

  defp sanitize_data_map(data) when is_map(data) do
    Map.new(data, fn {key, value} -> {key, sanitize_data_value(key, value)} end)
  end

  defp sanitize_data_value(key, value) when is_binary(value) do
    case Map.get(@data_string_max_lengths, key) do
      nil -> strip_null_bytes(value)
      max_length -> value |> strip_null_bytes() |> truncate_string(max_length)
    end
  end

  defp sanitize_data_value(_key, value), do: value

  # PostgreSQL JSONB does not support \u0000 (null bytes)
  defp strip_null_bytes(string) when is_binary(string) do
    String.replace(string, <<0x00>>, "")
  end

  defp truncate_string(string, max_length) when byte_size(string) <= max_length do
    string
  end

  defp truncate_string(string, max_length) do
    # Use binary_part to safely truncate at byte boundaries
    # This prevents splitting UTF-8 characters
    truncated = binary_part(string, 0, max_length)

    # Ensure we don't end in the middle of a UTF-8 character
    if String.valid?(truncated) do
      truncated
    else
      truncate_to_valid_utf8(string, max_length)
    end
  end

  defp truncate_to_valid_utf8(string, max_length) do
    # Work backwards from max_length to find a valid UTF-8 boundary
    truncate_to_valid_utf8(string, max_length - 1, max_length)
  end

  defp truncate_to_valid_utf8(_string, 0, _original_max), do: ""

  defp truncate_to_valid_utf8(string, current_length, original_max) do
    truncated = binary_part(string, 0, current_length)

    if String.valid?(truncated) do
      truncated
    else
      truncate_to_valid_utf8(string, current_length - 1, original_max)
    end
  end
end
