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

use Croma

defmodule AntikytheraCore do
  use Application
  alias Antikythera.GearActionTimeout
  alias AntikytheraEal.ClusterConfiguration
  require AntikytheraCore.Logger, as: L

  defun add_translator_to_logger() :: :ok do
    Logger.add_translator({AntikytheraCore.ErlangLogTranslator, :translate})
  end

  @doc """
  Callback implementation of `Application.start/2`.

  Interdependencies between initialization steps here are crucial.
  See also `AntikytheraCore.StartupManager` for initializations after construction
  of the supervision tree.
  """
  @impl true
  def start(_type, _args) do
    add_translator_to_logger()
    # In dev or local environment, the log level is initially set to `:notice` at mix_common.exs
    # in order to avoid SASL progress reports.
    # The log level is restored to `:info` after loading antikythera.
    Logger.configure(level: :info)
    L.info("starting AntikytheraCore")
    add_gears_dir_to_erl_libs()
    AntikytheraCore.FileSetup.setup_files_and_ets_tables()
    AntikytheraCore.Config.Core.load()
    # Just to suppress log messages by `:syn.add_node_to_scopes/1`.
    if not Antikythera.Env.no_listen?() do
      calculate_connection_trial_count_from_health_check_grace_period()
      |> establish_connections_to_other_nodes()

      :syn.add_node_to_scopes([:antikythera])
    end

    L.info("activating RaftFleet")

    activate_raft_fleet(fn ->
      if not Antikythera.Env.no_listen?() do
        start_cowboy_http()
      end

      {:ok, pid} = start_sup()
      # `GearManager` and `StartupManager` must be up and running here
      AntikytheraCore.Config.Gear.load_all(0)
      L.info("started AntikytheraCore")
      {:ok, pid}
    end)
  end

  defp add_gears_dir_to_erl_libs() do
    # Set ERL_LIBS environment variable in order to load gear's code appropriately.
    # See also: http://www.erlang.org/doc/man/code.html#lib_dir-1
    dirs = (System.get_env("ERL_LIBS") || "") |> String.split(":")
    new_value = [AntikytheraCore.Version.Artifact.gears_dir() | dirs] |> Enum.join(":")
    System.put_env("ERL_LIBS", new_value)
  end

  @connection_retrial_interval_in_milliseconds 5_000

  defunpt calculate_connection_trial_count_from_health_check_grace_period() :: pos_integer do
    connection_retrial_interval_in_seconds = @connection_retrial_interval_in_milliseconds / 1000

    (ClusterConfiguration.health_check_grace_period_in_seconds() /
       connection_retrial_interval_in_seconds)
    |> trunc()
    |> max(1)
  end

  defp establish_connections_to_other_nodes(tries_remaining) do
    if tries_remaining == 0 do
      raise "cannot establish connections to other nodes!"
    else
      case AntikytheraCore.Cluster.connect_to_other_nodes_on_start() do
        {:ok, true} ->
          :ok

        _otherwise ->
          L.info("failed to establish connections to other nodes; retry afterward")
          :timer.sleep(@connection_retrial_interval_in_milliseconds)
          establish_connections_to_other_nodes(tries_remaining - 1)
      end
    end
  end

  defp activate_raft_fleet(f) do
    :ok = RaftFleet.activate(ClusterConfiguration.zone_of_this_host())

    try do
      f.()
    catch
      type, reason ->
        # When an error occurred in the core part of `start/2`, try to cleanup this node so that
        # existing consensus groups (especially `RaftFleet.Cluster`) are not disturbed by the failing node.
        RaftFleet.deactivate()
        # wait for a moment in the hope that deactivation succeeds...
        :timer.sleep(10_000)
        {:error, {type, reason}}
    end
  end

  defp start_cowboy_http() do
    dispatch_rules = AntikytheraCore.Handler.CowboyRouting.compiled_routes([], false)

    ranch_transport_opts = %{
      # limit is imposed on a per-executor pool basis
      max_connections: :infinity,
      socket_opts: [port: Antikythera.Env.port_to_listen()]
    }

    cowboy_proto_opts = %{
      # timeout of a request with no data transfer; must be sufficiently longer than the gear action timeout
      idle_timeout: GearActionTimeout.max() + 20_000,
      # timeout of a connection with no requests; this should be longer than LB's idle timeout
      request_timeout: GearActionTimeout.max() + 60_000,
      env: %{dispatch: dispatch_rules},
      stream_handlers: [:cowboy_compress_h, :cowboy_stream_h]
    }

    {:ok, _} =
      :cowboy.start_clear(:antikythera_http_listener, ranch_transport_opts, cowboy_proto_opts)
  end

  defp start_sup() do
    children = [
      AntikytheraCore.ErrorCountsAccumulator,
      {AntikytheraCore.Alert.Manager, [:antikythera, AntikytheraCore.Alert.Manager]},
      AntikytheraCore.GearManager,
      AntikytheraCore.ClusterHostsPoller,
      AntikytheraCore.ClusterNodesConnector,
      AntikytheraCore.MnesiaNodesCleaner,
      AntikytheraCore.StartupManager,
      AntikytheraCore.TerminationManager,
      AntikytheraCore.CoreConfigPoller,
      AntikytheraCore.GearConfigPoller,
      AntikytheraCore.VersionUpgradeTaskQueue,
      AntikytheraCore.VersionSynchronizer,
      AntikytheraCore.StaleGearArtifactCleaner,
      {AntikytheraCore.MetricsUploader, [:antikythera, AntikytheraCore.MetricsUploader]},
      {AntikytheraCore.SystemMetricsReporter, [AntikytheraCore.MetricsUploader]},
      AntikytheraCore.ExecutorPool.Sup,
      AntikytheraCore.GearExecutorPoolsManager,
      AntikytheraCore.TenantExecutorPoolsManager,
      AntikytheraCore.TmpdirTracker,
      AntikytheraCore.ExecutorPool.AsyncJobLog.Writer
    ]

    children_for_dev =
      if Antikythera.Env.runtime_env() == :prod,
        do: [],
        else: [
          Supervisor.child_spec(
            {
              AntikytheraCore.PeriodicLog.Writer,
              [AntikytheraCore.PeriodicLog.ReductionBuilder, "reduction"]
            },
            id: :periodic_log_writer_1
          ),
          Supervisor.child_spec(
            {
              AntikytheraCore.PeriodicLog.Writer,
              [AntikytheraCore.PeriodicLog.MessageBuilder, "message"]
            },
            id: :periodic_log_writer_2
          )
        ]

    opts = [strategy: :one_for_one, name: AntikytheraCore.Supervisor]
    Supervisor.start_link(children ++ children_for_dev, opts)
  end
end
