local/running_environment.ex

# Copyright(c) 2015-2023 ACCESS CO., LTD. All rights reserved.

use Croma

defmodule AntikytheraLocal.RunningEnvironment do
  alias Croma.Result, as: R
  alias Antikythera.{Env, GearNameStr, VersionStr, Httpc, EnumUtil}
  alias AntikytheraCore.Path, as: CorePath
  alias AntikytheraLocal.{Cmd, StartScript}

  # Paths for test process that controls the ErlangVM that runs the release
  @release_output_dir "rel_local_erlang-#{System.otp_release()}"

  # Paths for the ErlangVM that runs the release
  current_os_pid = :os.getpid()

  @antikythera_root_dir CorePath.antikythera_root_dir()
                        |> String.replace(~r|/tmp/#{current_os_pid}/|, "/tmp/local/")
  @compiled_gears_dir CorePath.compiled_gears_dir()
                      |> String.replace(~r|/tmp/#{current_os_pid}/|, "/tmp/local/")
  @history_dir CorePath.history_dir()
               |> String.replace(~r|/tmp/#{current_os_pid}/|, "/tmp/local/")
  @system_info_access_token_path CorePath.system_info_access_token_path()
                                 |> String.replace(~r|/tmp/#{current_os_pid}/|, "/tmp/local/")
  @raft_fleet_dir CorePath.raft_persistence_dir_parent()
                  |> String.replace(~r|/tmp/#{current_os_pid}/|, "/tmp/local/")
  @release_dir Path.join([@antikythera_root_dir, "..", "release_per_node"]) |> Path.expand()
  @unpacked_gears_dir Path.join(@release_dir, "gears")

  def unpacked_gears_dir(), do: @unpacked_gears_dir

  @compile_environment_vars [{"ANTIKYTHERA_COMPILE_ENV", "local"}, {"MIX_ENV", "prod"}]

  defun setup(gear_repo_dirs :: [Path.t()]) :: :ok do
    create_dir_same_as_node()
    build_core_and_unpack_at_release_dir()
    StartScript.run("daemon", @release_dir)
    wait_until_directories_are_created()

    Enum.each(gear_repo_dirs, fn gear_repo_dir ->
      {gear_name_str, version} = build_gear_and_move_to_artifact_dir(gear_repo_dir, false)
      :ok = add_version_to_history_file(gear_name_str, version, true)
    end)

    IO.puts(
      "Successfully finished setting-up an OTP release for #{Env.antikythera_instance_name()}"
    )
  end

  defp wait_until_directories_are_created(n \\ 10) do
    if n == 0 do
      raise "Directories under #{@antikythera_root_dir} haven't been created!"
    else
      :timer.sleep(500)

      if File.dir?(@history_dir) do
        :ok
      else
        wait_until_directories_are_created(n - 1)
      end
    end
  end

  defun teardown() :: :ok do
    StartScript.run("stop", @release_dir)

    # Kill epmd (running under `@release_dir`) as it prevents us from removing `tmp/` when using NFS
    _ = System.cmd("pkill", ["-f", "#{Env.antikythera_instance_name()}.*epmd"])
    clear_local_running_dir_and_release_products()
  end

  defun prepare_new_version_of_gear(gear_repo_dir :: Path.t(), do_upgrade :: v[boolean]) ::
          String.t() do
    {gear_name_str, new_version} = build_gear_and_move_to_artifact_dir(gear_repo_dir, true)
    :ok = add_version_to_history_file(gear_name_str, new_version, do_upgrade)
    new_version
  end

  defun prepare_new_version_of_core(do_upgrade :: v[boolean]) :: VersionStr.t() do
    version = build_core()
    :ok = add_version_to_history_file("#{Env.antikythera_instance_name()}", version, do_upgrade)
    version
  end

  defun currently_running_os_process_ids() :: [String.t()] do
    script_basename = "#{Env.antikythera_instance_name()}.sh"
    {pids, _} = System.cmd("pgrep", ["-f", script_basename])
    pids |> String.split("\n", trim: true)
  end

  defun wait_until_upgrade_applied(app_name :: v[atom], sha1 :: v[String.t()]) :: :ok do
    wait_until_upgrade_applied_impl(Atom.to_string(app_name), sha1, 0)
  end

  defp wait_until_upgrade_applied_impl(app_name_str, sha1, count) do
    if count >= 20 do
      raise "Upgrade of #{app_name_str} to #{sha1} hasn't been applied!"
    else
      :timer.sleep(10_000)
      current_version = fetch_current_version(app_name_str)

      if R.ok?(current_version) and R.get!(current_version) |> String.ends_with?(sha1) do
        :ok
      else
        wait_until_upgrade_applied_impl(app_name_str, sha1, count + 1)
      end
    end
  end

  defp fetch_current_version(app_name_str) do
    url = "http://#{AntikytheraCore.Cmd.hostname()}:8080/versions"
    token = File.read!(@system_info_access_token_path)

    Httpc.get(url, %{"authorization" => token})
    |> R.map(fn %Httpc.Response{status: 200, body: body} ->
      extract_version_str(body, app_name_str)
    end)
  end

  defp extract_version_str(body, app_name_str) do
    String.split(body, "\n", trim: true)
    |> EnumUtil.find_value!(fn line ->
      case String.split(line) do
        [^app_name_str, v] -> v
        _ -> nil
      end
    end)
  end

  defp create_dir_same_as_node() do
    File.rm_rf!(@release_dir)
    File.mkdir_p!(@unpacked_gears_dir)
  end

  defp clear_local_running_dir_and_release_products() do
    File.rm_rf!(@release_dir)
    File.rm_rf!(@raft_fleet_dir)
    File.rm_rf!(@release_output_dir)
    :ok
  end

  defunp build_gear_and_move_to_artifact_dir(
           gear_repo_dir :: Path.t(),
           generate_appup? :: v[boolean]
         ) :: {GearNameStr.t(), VersionStr.t()} do
    gear_name_str = Path.basename(gear_repo_dir)
    build_gear_dir = build_gear(gear_name_str, gear_repo_dir)
    version = AntikytheraCore.Version.read_from_app_file(build_gear_dir, gear_name_str)
    IO.puts("Successfully built #{gear_name_str} (#{version})")
    if generate_appup?, do: generate_appup(gear_name_str, gear_repo_dir)
    rename_dir!(build_gear_dir, Path.join(@compiled_gears_dir, "#{gear_name_str}-#{version}"))

    {_, 0} =
      System.cmd(
        "tar",
        ["-czhf", "#{gear_name_str}-#{version}.tgz", "#{gear_name_str}-#{version}"],
        cd: @compiled_gears_dir
      )

    {gear_name_str, version}
  end

  defp rename_dir!(src, dest) do
    # `File.rename/2` won't work across partitions
    File.cp_r!(src, dest)
    File.rm_rf!(src)
  end

  defunp build_gear(gear_name_str :: v[GearNameStr.t()], gear_repo_dir :: Path.t()) :: Path.t() do
    # fetch antikythera instance
    Cmd.exec_and_output_log!("mix", ["deps.get"],
      cd: gear_repo_dir,
      env: @compile_environment_vars
    )

    # fetch antikythera (if changed)
    Cmd.exec_and_output_log!("mix", ["deps.get"],
      cd: gear_repo_dir,
      env: @compile_environment_vars
    )

    Cmd.exec_and_output_log!("mix", ["compile"], cd: gear_repo_dir, env: @compile_environment_vars)

    Path.join([gear_repo_dir, "_build_local", "prod", "lib", gear_name_str])
  end

  defunp generate_appup(gear_name_str :: v[GearNameStr.t()], gear_repo_dir :: Path.t()) :: :ok do
    {last_line, 0} = System.cmd("tail", ["-1", Path.join(@history_dir, gear_name_str)])
    current_version = String.trim_trailing(last_line) |> String.split(" ") |> hd()
    current_dir = Path.join(@compiled_gears_dir, "#{gear_name_str}-#{current_version}")

    Cmd.exec_and_output_log!("mix", ["antikythera_core.generate_appup", current_dir],
      cd: gear_repo_dir,
      env: @compile_environment_vars
    )
  end

  defp build_core_and_unpack_at_release_dir() do
    instance_name = Env.antikythera_instance_name()
    version = build_core()
    dest = Path.join(@release_dir, "#{instance_name}.tar.gz")

    File.cp!(
      Path.join([
        @release_output_dir,
        "#{instance_name}",
        "releases",
        version,
        "#{instance_name}.tar.gz"
      ]),
      dest
    )

    Cmd.exec_and_output_log!("tar", ["-xf", "#{instance_name}.tar.gz"], cd: @release_dir)
    File.rm!(dest)
  end

  defp build_core() do
    Cmd.exec_and_output_log!("mix", ["antikythera_core.generate_release"],
      env: @compile_environment_vars
    )

    version = get_core_version_from_release_file()
    IO.puts("Successfully built core (#{version})")
    version
  end

  defp get_core_version_from_release_file() do
    instance_name_charlist = Env.antikythera_instance_name() |> Atom.to_charlist()

    releases_file_path =
      Path.join([@release_output_dir, "#{instance_name_charlist}", "releases", "RELEASES"])

    {:ok, [term]} = :file.consult(String.to_charlist(releases_file_path))
    [{:release, ^instance_name_charlist, version, _, _, _}] = term
    List.to_string(version)
  end

  defunp add_version_to_history_file(
           app_name :: v[String.t()],
           version :: v[VersionStr.t()],
           do_upgrade :: v[boolean]
         ) :: :ok do
    history_file_path = Path.join(@history_dir, app_name)
    line = if do_upgrade, do: version, else: "#{version} noupgrade"
    :ok = File.write(history_file_path, "#{line}\n", [:append])
  end
end