defmodule Vaultex.Client do
  @moduledoc """
  Provides a functionality to authenticate and read from a vault endpoint.
  """

  use GenServer
  alias Vaultex.Auth, as: Auth
  alias Vaultex.Read, as: Read
  alias Vaultex.Write, as: Write
  alias Vaultex.Delete, as: Delete
  alias Vaultex.Leases, as: Leases
  @version "v1"

  def start_link() do
    GenServer.start_link(__MODULE__, %{progress: "starting"}, name: :vaultex)
  end

  def init(state) do
    {:ok, Map.merge(state, %{url: url()})}
  end

  @doc """
  Authenticates with vault using a tuple. This can be executed before attempting to read secrets from vault.

  ## Parameters

    - method: Auth backend to use for authenticating, can be one of `:approle, :app_id, :userpass, :github, :token`
    - credentials: A tuple or map used for authentication depending on the method, `{role_id, secret_id}` for `:approle`, `{app_id, user_id}` for `:app_id`, `{username, password}` for `:userpass`, `{github_token}` for `:github`, `{token}` for `:token`, or json-encodable map for unhandled methods, i.e. `%{jwt: "jwt", role: "role"}` for `:kubernetes`
    - timeout: A integer greater than zero which specifies how many milliseconds to wait for a reply

  ## Examples

      iex> Vaultex.Client.auth(:approle, {role_id, secret_id}, 5000)
      {:ok, :authenticated}

      iex> Vaultex.Client.auth(:app_id, {app_id, user_id})
      {:ok, :authenticated}

      iex> Vaultex.Client.auth(:userpass, {username, password})
      {:error, ["Something didn't work"]}

      iex> Vaultex.Client.auth(:github, {github_token})
      {:ok, :authenticated}

      iex> Vaultex.Client.auth(:jwt, %{jwt: jwt, role: role})
      {:ok, :authenticated}
  """
  @spec auth(method :: :approle, credentials :: {role_id :: String.t, secret_id :: String.t}, timeout :: String.t | nil) :: {:ok | :error, any}
  @spec auth(method :: :app_id, credentials :: {app_id :: String.t, user_id :: String.t}, timeout :: String.t | nil) :: {:ok | :error, any}
  @spec auth(method :: :userpass, credentials :: {username :: String.t, password :: String.t}, timeout :: String.t | nil) :: {:ok | :error, any}
  @spec auth(method :: :github, credentials :: {github_token :: String.t}, timeout :: String.t | nil) :: {:ok | :error, any}
  @spec auth(method :: :token, credentials :: {token :: String.t}, timeout :: String.t | nil) :: {:ok, :authenticated}
  @spec auth(method :: atom, credentials :: map) :: {:ok | :error, any}
  def auth(method, credentials, timeout \\ 5000) do
    GenServer.call(:vaultex, {:auth, method, credentials}, timeout)
  end

  @doc """
  Reads a secret from vault given a path.

  ## Parameters

    - key: A String path to be used for querying vault.
    - auth_method and credentials: See Vaultex.Client.auth
    - timeout: A integer greater than zero which specifies how many milliseconds to wait for a reply

  ## Examples

      iex> Vaultex.Client.read("secret/foobar", :approle, {role_id, secret_id}, 5000)
      {:ok, %{"value" => "bar"}}

      iex> Vaultex.Client.read("secret/foo", :app_id, {app_id, user_id})
      {:ok, %{"value" => "bar"}}

      iex> Vaultex.Client.read("secret/baz", :userpass, {username, password})
      {:error, ["Key not found"]}

      iex> Vaultex.Client.read("secret/bar", :github, {github_token})
      {:ok, %{"value" => "bar"}}

      iex> Vaultex.Client.read("secret/bar", :plugin_defined_auth, credentials)
      {:ok, %{"value" => "bar"}}
  """
  def read(key, auth_method, credentials, timeout \\ 5000) do
    case read_resp(key, auth_method, credentials, timeout) do
      {:ok, %{"data" => data}} -> {:ok, data}
      not_okay -> not_okay
    end
  end

  @doc """
  Reads a dynamic secret from vault given a path and returns the secret along with lease information.

  ## Parameters

    - key: A String path to be used for querying vault.
    - auth_method and credentials: See Vaultex.Client.auth
    - timeout: A integer greater than zero which specifies how many milliseconds to wait for a reply

  ## Examples

      iex> Vaultex.Client.read_dynamic("secret/dynamic/foobar", :approle, {role_id, secret_id}, 5000)
      {:ok, %{"data" => %{"value" => "bar"}, "lease_duration" => 60, "lease_id" => "secret/dynamic/foo/b4z", "renewable" => true}}

      iex> Vaultex.Client.read_dynamic("secret/dynamic/foo", :app_id, {app_id, user_id})
      {:ok, %{"data" => %{"value" => "bar"}, "lease_duration" => 60, "lease_id" => "secret/dynamic/foo/b4z", "renewable" => true}}

      iex> Vaultex.Client.read_dynamic("secret/dynamic/baz", :userpass, {username, password})
      {:error, ["Key not found"]}

      iex> Vaultex.Client.read_dynamic("secret/dynamic/bar", :github, {github_token})
      {:ok, %{"data" => %{"value" => "bar"}, "lease_duration" => 60, "lease_id" => "secret/dynamic/foo/b4z", "renewable" => true}}

      iex> Vaultex.Client.read_dynamic("secret/dynamic/bar", :plugin_defined_auth, credentials)
      {:ok, %{"data" => %{"value" => "bar"}, "lease_duration" => 60, "lease_id" => "secret/dynamic/foo/b4z", "renewable" => true}}
  """
  def read_dynamic(key, auth_method, credentials, timeout \\ 5000) do
    read_resp(key, auth_method, credentials, timeout)
  end


  defp read_resp(key, auth_method, credentials, timeout) do
    response = read(key, timeout)
    case response do
      {:ok, _} -> response
      {:error, _} ->
        with {:ok, _} <- auth(auth_method, credentials, timeout),
          do: read(key, timeout)
    end
  end

  defp read(key, timeout) do
    GenServer.call(:vaultex, {:read, key}, timeout)
  end

  @doc """
  Renews a lease for a dynamic secret

  ## Parameters

    - lease_id: A String that is the lease ID returned when reading a dynamic secret
    - increment: An Integer that represents the time in seconds to extend the lease by
    - auth_method and credentials: See Vaultex.Client.auth
    - timeout: A integer greater than zero which specifies how many milliseconds to wait for a reply

  ## Examples

      iex> Vaultex.Client.renew_lease("secret/dynamic/foo/b4z", 100, :app_role, {role_id, secret_id}, 5000)
      {:ok, %{"lease_id" => "secret/dynamic/foo/b4z", "lease_duration" => 160, "renewable" => true}}
  """

  def renew_lease(lease_id, increment, auth_method, credentials, timeout \\ 5000) do
    response = renew_lease(lease_id, increment, timeout)
    case response do
      {:ok, _} -> response
      {:error, _} ->
        with {:ok, _} <- auth(auth_method, credentials, timeout),
          do: renew_lease(lease_id, increment, timeout)
    end
  end

  defp renew_lease(lease_id, increment, timeout) do
    GenServer.call(:vaultex, {:renew, lease_id, increment}, timeout)
  end

  @doc """
  Writes a secret to Vault given a path.

  ## Parameters

    - key: A String path where the secret will be written.
    - value: A String => String map that will be stored in Vault
    - auth_method and credentials: See Vaultex.Client.auth
    - timeout: A integer greater than zero which specifies how many milliseconds to wait for a reply

  ## Examples

      iex> Vaultex.Client.write("secret/foo", %{"value" => "bar"}, :app_role, {role_id, secret_id}, 5000)
      :ok

      iex> Vaultex.Client.write("secret/foo", %{"value" => "bar"}, :app_id, {app_id, user_id})
      :ok
  """
  def write(key, value, auth_method, credentials, timeout \\ 5000) do
    response = write(key, value, timeout)
    case response do
      :ok -> response
      {:ok, response} -> {:ok, response}
      {:error, _} ->
        with {:ok, _} <- auth(auth_method, credentials, timeout),
          do: write(key, value, timeout)
    end
  end

  defp write(key, value, timeout) do
    GenServer.call(:vaultex, {:write, key, value}, timeout)
  end

  @doc """
  Deletes a secret in Vault given a path.

  ## Parameters

    - key: A String path where the secret will be deleted.
    - auth_method and credentials: See Vaultex.Client.auth
    - timeout: A integer greater than zero which specifies how many milliseconds to wait for a reply

  ## Examples

      iex> Vaultex.Client.delete("secret/foo", :app_role, {role_id, secret_id}, 5000)
      :ok

      iex> Vaultex.Client.delete("secret/foo", :app_id, {app_id, user_id})
      :ok
  """
  def delete(key, auth_method, credentials, timeout \\ 5000) do
    response = delete(key, timeout)
    case response do
      :ok -> response
      {:error, _} ->
        with {:ok, _} <- auth(auth_method, credentials, timeout),
          do: delete(key, timeout)
    end
  end

  defp delete(key, timeout) do
    GenServer.call(:vaultex, {:delete, key}, timeout)
  end

  def handle_call({:read, key}, _from, state) do
    Read.handle(key, state)
  end

  def handle_call({:renew, lease_id, increment}, _from, state) do
    Leases.handle(:renew, lease_id, increment, state)
  end

  def handle_call({:write, key, value}, _from, state) do
    Write.handle(key, value, state)
  end

  def handle_call({:auth, method, credentials}, _from, state) do
    Auth.handle(method, credentials, state)
  end

  def handle_call({:delete, key}, _from, state) do
    Delete.handle(key, state)
  end

  defp url do
    "#{scheme()}://#{host()}:#{port()}/#{@version}/"
  end

  defp host do
    parsed_vault_addr().host || get_env(:host)
  end

  defp port do
    parsed_vault_addr().port || get_env(:port)
  end

  defp scheme do
    parsed_vault_addr().scheme || get_env(:scheme)
  end

  defp parsed_vault_addr do
    get_env(:vault_addr) |> to_string |> URI.parse
  end

  defp get_env(:host) do
    System.get_env("VAULT_HOST") || Application.get_env(:vaultex, :host) || "localhost"
  end

  defp get_env(:port) do
      System.get_env("VAULT_PORT") || Application.get_env(:vaultex, :port) || 8200
  end

  defp get_env(:scheme) do
      System.get_env("VAULT_SCHEME") || Application.get_env(:vaultex, :scheme) || "http"
  end

  defp get_env(:vault_addr) do
    Application.get_env(:vaultex, :vault_addr) || System.get_env("VAULT_ADDR")
  end
end
