defmodule Hologram.Runtime.Session do
  @moduledoc false

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

  @type op :: :delete | {:put, any}

  @doc """
  Initializes or retrieves an existing session ID from the "hologram_session" cookie.

  Returns `{conn, session_id}` where:
  - `conn` is the connection with the session cookie set
  - `session_id` is the UUID of the session
  """
  @spec init(Plug.Conn.t()) :: {Plug.Conn.t(), String.t()}
  def init(initial_conn) do
    # This is idempotent
    conn = Plug.Conn.fetch_cookies(initial_conn)

    case conn.req_cookies["hologram_session"] do
      nil ->
        create_new_session(conn)

      encrypted_session ->
        case decrypt_session_data(encrypted_session) do
          {:ok, session_data} ->
            session_id = session_data.session_id
            {conn, session_id}

          {:error, _reason} ->
            create_new_session(conn)
        end
    end
  end

  defp create_new_session(conn) do
    session_id = UUID.uuid4()
    session_data = %{session_id: session_id}
    encrypted_session = encrypt_session_data(session_data)
    new_conn = put_session_cookie(conn, encrypted_session)

    {new_conn, session_id}
  end

  # sobelow_skip ["Misc.BinToTerm"]
  defp decrypt_session_data(encrypted_data) do
    encryption_key = derive_encryption_key()

    # last param (sign key) in encrypt/3 is not used
    case MessageEncryptor.decrypt(encrypted_data, encryption_key, "") do
      {:ok, decrypted_data} ->
        {:ok, :erlang.binary_to_term(decrypted_data)}

      :error ->
        {:error, :invalid_session}
    end
  rescue
    _error -> {:error, :invalid_session}
  end

  defp derive_encryption_key do
    "SECRET_KEY_BASE"
    |> System.fetch_env!()
    |> KeyGenerator.generate("hologram session")
  end

  defp encrypt_session_data(data) do
    serialized_data = :erlang.term_to_binary(data, compressed: 6)
    encryption_key = derive_encryption_key()

    # last param (sign key) in encrypt/3 is not used
    MessageEncryptor.encrypt(serialized_data, encryption_key, "")
  end

  defp put_session_cookie(conn, encrypted_session) do
    Plug.Conn.put_resp_cookie(conn, "hologram_session", encrypted_session,
      http_only: true,
      same_site: "Lax",
      secure: conn.scheme == :https
    )
  end
end
