defmodule Drab.Live.Crypto do
  @moduledoc false

  alias Plug.Crypto.KeyGenerator
  alias Plug.Crypto.MessageEncryptor

  @doc false
  @spec uuid :: String.t()
  @spec uuid(String.t()) :: String.t()
  def uuid(begin_with \\ "u"), do: begin_with <> hash({now_ms(), make_ref()})

  # The most effective way for store assigns in the browser is basic encode
  @doc false
  @spec encode32(term) :: String.t()
  def encode32(term) do
    term |> :erlang.term_to_binary() |> Base.encode32(padding: false, case: :lower)
  end

  @doc false
  @spec decode32(String.t()) :: term()
  def decode32(string) do
    string |> Base.decode32!(padding: false, case: :lower) |> :erlang.binary_to_term()
  end

  @doc false
  @spec encode64(term) :: String.t()
  def encode64(term) do
    # term |> :erlang.term_to_binary() |> Base.url_encode64()
    encrypt(term)
  end

  @doc false
  @spec decode64(String.t()) :: term()
  def decode64(string) do
    # string |> Base.url_decode64!() |> :erlang.binary_to_term()
    decrypt(string)
  end

  @doc false
  @spec encrypt(term) :: String.t()
  def encrypt(term) do
    {secret, sign_secret} = keys()
    MessageEncryptor.encrypt(:erlang.term_to_binary(term), secret, sign_secret)
  end

  @doc false
  @spec encrypt(term, atom, String.t()) :: String.t()
  def encrypt(term, endpoint, salt) do
    {secret, sign_secret} = keys(endpoint, salt)
    MessageEncryptor.encrypt(:erlang.term_to_binary(term), secret, sign_secret)
  end

  @doc false
  @spec decrypt(String.t()) :: term
  def decrypt(crypted) do
    {secret, sign_secret} = keys()

    case MessageEncryptor.decrypt(crypted, secret, sign_secret) do
      {:ok, decrypted} -> :erlang.binary_to_term(decrypted)
      :error -> :error
    end
  end

  @doc false
  @spec decrypt(String.t(), atom, String.t()) :: term
  def decrypt(crypted, endpoint, salt) do
    {secret, sign_secret} = keys(endpoint, salt)

    case MessageEncryptor.decrypt(crypted, secret, sign_secret) do
      {:ok, decrypted} -> :erlang.binary_to_term(decrypted)
      :error -> :error
    end
  end

  @doc false
  @spec keys :: {String.t(), String.t()}
  def keys() do
    secret_key_base = Drab.Config.secret_key_base()
    secret = KeyGenerator.generate(secret_key_base, "Drab.Live.Crypto salt")
    sign_secret = KeyGenerator.generate(secret_key_base, "Drab.Live.Crypto sign salt")
    {secret, sign_secret}
  end

  @doc false
  @spec keys(atom, String.t()) :: {String.t(), String.t()}
  def keys(endpoint, salt) do
    secret_key_base = Drab.Config.secret_key_base(endpoint)
    secret = KeyGenerator.generate(secret_key_base, "#{salt} salt")
    sign_secret = KeyGenerator.generate(secret_key_base, "#{salt} sign salt")
    {secret, sign_secret}
  end

  @spec now_ms :: integer
  defp now_ms(), do: System.system_time(:millisecond)

  @doc false
  @spec hash(term) :: String.t()
  def hash(term) do
    term
    |> :erlang.phash2(4_294_967_296)
    |> to_string()
    |> Base.encode32(padding: false, case: :lower)
  end
end
