require Logger

defmodule ElixirStan do
  @moduledoc """
  Documentation for `ElixirStan`.
  """

  @doc """
  Get the version of the stanc3 executable bundled with the library.
  """
  @spec stanc_version :: String.t() | {:error, String.t()}
  def stanc_version() do
    case System.cmd(Application.app_dir(:elixir_stan, "/priv/bin/stanc"), ["--version"]) do
      {version_string, 0} -> String.trim(version_string)
      {error, _} -> {:error, error}
    end
  end

  @doc """
  Computes the hash of a file used for caching.

  Computes SHA256 of the file and truncates it to 8 bytes, then encodes it in a hex string.
  """
  @spec hash_file(binary()) :: String.t()
  def hash_file(file) when is_binary(file) do
    <<hash::bitstring-size(64), _::binary>> = :crypto.hash(:sha256, file)

    Base.encode16 hash
  end

  @spec model_tmp_dir(Path.t()) :: binary | {:error, atom}
  def model_tmp_dir(path) do
    with {:ok, model_code} <- File.read(path) do
      model_name = Path.basename(path, ".stan")
      model_hash = hash_file(model_code)

      Path.join([
        System.tmp_dir(),
        "elixir_stan",
        model_name <> model_hash <> String.replace(stanc_version(), " ", "_")
      ])
    end
  end

  @doc """
  Transpiles the model into C++ and stores it in a temporary directory.

  Temporary directory is based on `System.tmp_dir()`. An `elixir_stan` directory is created in the system temporary
  directory and the model is stored in a subdirectory of that. The model directory is based on the model name, the hash
  of the model code and the version of stanc used to compile it.
  """
  @spec transpile_model(Path.t()) :: :ok | {:error, any()}
  def transpile_model(path) do
    with {:ok, model_code} <- File.read(path) do
      model_tmp_path = model_tmp_dir(path)

      # Create model dir
      :ok = File.mkdir_p(model_tmp_path)

      model_tmp_file = Path.join(model_tmp_path, Path.basename(path))

      # Write model code
      :ok = File.write(model_tmp_file, model_code)

      case System.cmd(Application.app_dir(:elixir_stan, "/priv/bin/stanc"), [model_tmp_file]) do
        {_, 0} -> :ok
      end
    end
  end

  @doc """
  Cleans the model tmp directory.
  """
  @spec clean_model(Path.t()) :: :ok | {:error, String.t()}
  def clean_model(path) do
    model_tmp_path = model_tmp_dir(path)

    # Remove model dir
    case File.rm_rf(model_tmp_path) do
      {:ok, _} ->
        :ok

      {:error, error, file} ->
        Logger.error(
          "Could not clean model: #{path}\n\t Error: #{:file.format_error(error)}\n\t File: #{file}"
        )

        {:error, :file.format_error(error)}
    end
  end
end
