defmodule Dotenv do
  @moduledoc """
  This module implements both an OTP application API and a "serverless" API.

  ## Server API

  Start the application with `start/2` On starting, it will automatically export
  the environment variables in the default path (`.env`).

  The environment can then be reloaded with `reload!/0` or a specific path
  or list of paths can be provided to `reload!/1`.

  ## Serverless API

  To use the serverless API, you can either load the environment variables with
  `load!` (again, optionally passing in a path or list of paths), or you
  can retrieve the variables without exporting them using `load`.
  """

  use Application
  alias Dotenv.Env

  def start(_type, env_path \\ :automatic) do
    Dotenv.Supervisor.start_link(env_path)
  end

  @quotes_pattern ~r/^(['"])(.*)\1$/
  @pattern ~r/
    \A
    (?:export\s+)?    # optional export
    ([\w\.]+)         # key
    (?:\s*=\s*|:\s+?) # separator
    (                 # optional value begin
      '(?:\'|[^'])*?' #   single quoted value
      |               #   or
      "(?:\"|[^"])*?" #   double quoted value
      |               #   or
      [^#\n]+?        #   unquoted value
    )?                # value end
    (?:\s*\#.*)?      # optional comment
    \z
    /x

  # https://regex101.com/r/XrvCwE/1
  @env_expand_pattern ~r/
    (?:^|[^\\])                           # prevent to expand \\$
    (                                     # get variable key pattern
      \$                                  #
      (?:                                 #
        ([A-Z0-9_]*[A-Z_]+[A-Z0-9_]*)     # get variable key
        |                                 #
        (?:                               #
          {([A-Z0-9_]*[A-Z_]+[A-Z0-9_]*)} # get variable key between {}
        )                                 #
      )                                   #
    )                                     #
    /x

  ##############################################################################
  # Server API
  ##############################################################################

  @doc """
  Calls the server to reload the values in the `.env` file into the
  system environment.

  This call is asynchronous (`cast`).
  """
  @spec reload!() :: :ok
  def reload! do
    :gen_server.cast(:dotenv, :reload!)
  end

  @doc """
  Calls the server to reload the values in the file located at `env_path` into
  the system environment.

  This call is asynchronous (`cast`).
  """
  @spec reload!(any) :: :ok
  def reload!(env_path) do
    :gen_server.cast(:dotenv, {:reload!, env_path})
  end

  @doc """
  Returns the current state of the server as a `Dotenv.Env` struct.
  """
  @spec env() :: Env.t()
  def env do
    :gen_server.call(:dotenv, :env)
  end

  @doc """
  Retrieves the value of the given `key` from the server, or `fallback` if the
  value is not found.
  """
  @spec get(String.t(), String.t() | nil) :: String.t()
  def get(key, fallback \\ nil) do
    :gen_server.call(:dotenv, {:get, key, fallback})
  end

  ##############################################################################
  # Serverless API
  ##############################################################################

  @doc """
  Reads the env files at the provided `env_path` path(s), exports the values into
  the system environment, and returns them in a `Dotenv.Env` struct.
  """
  def load!(env_path \\ :automatic) do
    env = load(env_path)
    System.put_env(env.values)
    env
  end

  @doc """
  Reads the env files at the provided `env_path` path(s) and returns the values
  in a `Dotenv.Env` struct.
  """
  @spec load(String.t() | :automatic | [String.t()]) :: Env.t()
  def load(env_path \\ :automatic)

  def load([env_path | env_paths]) do
    first_env = load(env_path)
    rest_env = load(env_paths)

    %Env{paths: [env_path | rest_env.paths], values: Map.merge(first_env.values, rest_env.values)}
  end

  def load([]) do
    %Env{paths: [], values: Map.new()}
  end

  def load(env_path) do
    {env_path, contents} = read_env_file(env_path)
    values = contents |> parse_contents()
    %Env{paths: [env_path], values: values}
  end

  def parse_contents(contents) do
    values = String.split(contents, "\n")

    values
    |> Enum.flat_map(&Regex.scan(@pattern, &1))
    |> trim_quotes_from_values
    |> Enum.reduce([], &expand_env/2)
    |> Enum.reduce(Map.new(), &collect_into_map/2)
  end

  defp collect_into_map([_whole, k, v], env), do: Map.put(env, k, v)
  defp collect_into_map([_whole, _k], env), do: env

  defp trim_quotes_from_values(values) do
    values
    |> Enum.map(fn values ->
      Enum.map(values, &trim_quotes/1)
    end)
  end

  defp trim_quotes(value) do
    String.replace(value, @quotes_pattern, "\\2")
  end

  # without value
  defp expand_env([_whole, _k], acc), do: acc

  defp expand_env([whole, k, v], acc) do
    matchs = Regex.scan(@env_expand_pattern, v)

    new_value =
      case Enum.empty?(matchs) do
        true ->
          v

        false ->
          matchs
          |> Enum.reduce(v, fn [_whole, pattern | keys], v ->
            v |> replace_env(pattern, keys, acc)
          end)
      end

    acc ++ [[whole, k, new_value]]
  end

  defp replace_env(value, pattern, ["" | keys], env), do: replace_env(value, pattern, keys, env)
  defp replace_env(value, pattern, [key | _], env), do: replace_env(value, pattern, key, env)

  defp replace_env(value, pattern, key, %Env{} = env) do
    new_value = env |> Env.get(key) || ""

    pattern
    |> Regex.escape()
    |> Regex.compile!()
    |> Regex.replace(value, new_value)
  end

  defp replace_env(value, pattern, key, acc) when is_list(acc) do
    values = acc |> Enum.reduce(Map.new(), &collect_into_map/2)
    replace_env(value, pattern, key, %Env{values: values})
  end

  defp replace_env(value, pattern, key, %{} = values) do
    replace_env(value, pattern, key, %Env{values: values})
  end

  defp read_env_file(:automatic) do
    case find_env_path() do
      {:ok, env_path} -> {env_path, File.read!(env_path)}
      {:error, _} -> {:none, ""}
    end
  end

  defp read_env_file(:none) do
    {:none, ""}
  end

  defp read_env_file(env_path) do
    {env_path, File.read!(env_path)}
  end

  defp find_env_path do
    find_env_path(File.cwd!())
  end

  defp find_env_path(dir) do
    candidate = Path.join(dir, ".env")

    cond do
      File.exists?(candidate) -> {:ok, candidate}
      dir == "/" -> {:error, "No .env found"}
      true -> find_env_path(Path.dirname(dir))
    end
  end
end
