defmodule Carve.HashIds do
  @moduledoc """
  Carve.HashIds provides functionality for encoding and decoding IDs using HashIds.

  This module is responsible for:
  - Configuring the HashIds settings
  - Encoding integer IDs into hashed strings
  - Decoding hashed strings back into integer IDs
  - Handling entity-specific salting for added security

  It uses the Hashids library internally and provides a convenient API for Carve users.
  """

  @alphabet "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
  @default_min_length 4
  @default_salt "1207:Rumi"

  @doc """
  Configures the HashIds settings.

  This function sets up the salt and minimum length for HashIds encoding.
  It stores these settings in persistent term storage for efficient access.

  ## Parameters

  - `opts`: A keyword list of configuration options.
    - `:salt` - The salt used for hashing (default: #{@default_salt})
    - `:min_length` - The minimum length of generated hashes (default: #{@default_min_length})

  ## Example

      iex> Carve.HashIds.configure(salt: "my_custom_salt", min_length: 8)
      :ok
  """
  def configure(opts) do
    salt = Keyword.get(opts, :salt, @default_salt)
    min_length = Keyword.get(opts, :min_length, @default_min_length)
    :persistent_term.put(:carve_hash_ids_salt, salt)
    :persistent_term.put(:carve_hash_ids_min_length, min_length)
  end

  @doc """
  Encodes an integer ID for a given type into a hashed string.

  ## Parameters

  - `type`: An atom representing the entity type (e.g., :user, :post)
  - `id`: The integer ID to be encoded

  ## Returns

  A string representing the encoded ID.

  ## Example

      iex> Carve.HashIds.encode(:user, 123)
      "Xk9Lp2Rr4m"
  """
  def encode(type, id) when is_atom(type) and is_integer(id) do
    hashids = provider()
    entity_salt = entity_salt(type)
    Hashids.encode(hashids, [id, entity_salt])
  end

  @doc """
  Decodes a hashed string back into an integer ID for a given type.

  ## Parameters

  - `type`: An atom representing the entity type (e.g., :user, :post)
  - `hash`: The hashed string to be decoded

  ## Returns

  `{:ok, id}` if decoding is successful, where `id` is the original integer ID.
  `{:error, :invalid_entity_type}` if the decoded salt doesn't match the given type.
  `{:error, reason}` for other decoding errors.

  ## Example

      iex> Carve.HashIds.decode(:user, "Xk9Lp2Rr4m")
      {:ok, 123}
  """
  def decode(type, hash) when is_atom(type) and is_binary(hash) do
    hashids = provider()
    entity_salt = entity_salt(type)
    case Hashids.decode(hashids, hash) do
      {:ok, [id, ^entity_salt]} -> {:ok, id}
      {:ok, _} -> {:error, :invalid_entity_type}
      {:error, _} = error -> error
    end
  end

  @doc """
  Decodes a hashed string without specifying the entity type.

  This function attempts to decode the hash without verifying the entity type.

  ## Parameters

  - `hash`: The hashed string to be decoded

  ## Returns

  `{:ok, id}` if decoding is successful, where `id` is the original integer ID.
  `{:error, reason}` for decoding errors.

  ## Example

      iex> Carve.HashIds.decode("Xk9Lp2Rr4m")
      {:ok, 123}
  """
  def decode(hash) when is_binary(hash) do
    hashids = provider()
    case Hashids.decode(hashids, hash) do
      {:ok, [id | _]} -> {:ok, id}
      {:error, _} = error -> error
    end
  end

  defp entity_salt(type) do
    type
    |> Atom.to_string()
    |> :erlang.phash2()
  end

  defp provider do
    salt = :persistent_term.get(:carve_hash_ids_salt)
    min_length = :persistent_term.get(:carve_hash_ids_min_length)
    Hashids.new(salt: salt, min_len: min_length, alphabet: @alphabet)
  end
end
