defmodule SimpleshopTheme.Vault do
  @moduledoc """
  Handles encryption and decryption of sensitive data.

  Uses AES-256-GCM for authenticated encryption.
  Keys are derived from the application's secret_key_base.
  """

  @aad "SimpleshopTheme.Vault"

  @doc """
  Encrypts a string value.

  Returns `{:ok, encrypted_binary}` or `{:error, reason}`.
  The encrypted binary includes the IV and auth tag.
  """
  @spec encrypt(String.t()) :: {:ok, binary()} | {:error, term()}
  def encrypt(plaintext) when is_binary(plaintext) do
    key = derive_key()
    iv = :crypto.strong_rand_bytes(12)

    {ciphertext, tag} =
      :crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, plaintext, @aad, true)

    # Format: iv (12 bytes) + tag (16 bytes) + ciphertext
    {:ok, iv <> tag <> ciphertext}
  rescue
    e -> {:error, e}
  end

  def encrypt(nil), do: {:ok, nil}

  @doc """
  Decrypts an encrypted binary.

  Returns `{:ok, plaintext}` or `{:error, reason}`.
  """
  @spec decrypt(binary()) :: {:ok, String.t()} | {:error, term()}
  def decrypt(<<iv::binary-12, tag::binary-16, ciphertext::binary>>) do
    key = derive_key()

    case :crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, ciphertext, @aad, tag, false) do
      plaintext when is_binary(plaintext) ->
        {:ok, plaintext}

      :error ->
        {:error, :decryption_failed}
    end
  rescue
    e -> {:error, e}
  end

  def decrypt(nil), do: {:ok, nil}
  def decrypt(""), do: {:ok, ""}

  def decrypt(_invalid) do
    {:error, :invalid_ciphertext}
  end

  @doc """
  Encrypts a string value, raising on error.
  """
  @spec encrypt!(String.t()) :: binary()
  def encrypt!(plaintext) do
    case encrypt(plaintext) do
      {:ok, ciphertext} -> ciphertext
      {:error, reason} -> raise "Encryption failed: #{inspect(reason)}"
    end
  end

  @doc """
  Decrypts an encrypted binary, raising on error.
  """
  @spec decrypt!(binary()) :: String.t()
  def decrypt!(ciphertext) do
    case decrypt(ciphertext) do
      {:ok, plaintext} -> plaintext
      {:error, reason} -> raise "Decryption failed: #{inspect(reason)}"
    end
  end

  # Derives a 32-byte key from the secret_key_base
  defp derive_key do
    secret_key_base = get_secret_key_base()

    :crypto.hash(:sha256, secret_key_base <> "vault_encryption_key")
  end

  defp get_secret_key_base do
    case Application.get_env(:simpleshop_theme, SimpleshopThemeWeb.Endpoint)[:secret_key_base] do
      nil ->
        raise """
        Secret key base is not configured.
        Set it in config/runtime.exs or config/dev.exs.
        """

      key when is_binary(key) ->
        key
    end
  end
end
