defmodule Berrypod.Analytics.Salt do
  @moduledoc """
  Daily-rotating salt for privacy-friendly visitor hashing.

  Generates a random 32-byte salt on startup and rotates it at midnight UTC.
  The visitor_hash is SHA256(salt + IP + UA) truncated to 8 bytes — enough
  for unique visitor counting without being reversible. Because the salt
  changes daily, the same visitor gets a different hash each day.
  """

  use GenServer

  @salt_bytes 32
  @hash_bytes 8

  # ── Public API ──

  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @doc """
  Hashes a visitor's IP and user agent into an 8-byte binary.
  """
  def hash_visitor(ip, user_agent) when is_tuple(ip) do
    hash_visitor(:inet.ntoa(ip) |> to_string(), user_agent)
  end

  def hash_visitor(ip, user_agent) when is_binary(ip) and is_binary(user_agent) do
    salt = GenServer.call(__MODULE__, :get_salt)

    :crypto.hash(:sha256, [salt, ip, user_agent])
    |> binary_part(0, @hash_bytes)
  end

  # ── GenServer callbacks ──

  @impl true
  def init(_opts) do
    schedule_rotation()
    {:ok, %{salt: generate_salt()}}
  end

  @impl true
  def handle_call(:get_salt, _from, state) do
    {:reply, state.salt, state}
  end

  @impl true
  def handle_info(:rotate, _state) do
    schedule_rotation()
    {:noreply, %{salt: generate_salt()}}
  end

  # ── Private ──

  defp generate_salt, do: :crypto.strong_rand_bytes(@salt_bytes)

  defp schedule_rotation do
    ms_until_midnight = ms_until_next_midnight()
    Process.send_after(self(), :rotate, ms_until_midnight)
  end

  defp ms_until_next_midnight do
    now = DateTime.utc_now()

    midnight =
      now
      |> DateTime.to_date()
      |> Date.add(1)
      |> DateTime.new!(~T[00:00:00], "Etc/UTC")

    DateTime.diff(midnight, now, :millisecond)
  end
end
