defmodule Actors.Actor.Entity.Lifecycle.StreamInitiator do
  @moduledoc """
  Handles lifecycle functions for Actor Entity that interacts with Event Source mechanisms
  """
  require Logger

  alias Actors.Actor.Entity.Lifecycle.StreamConsumer

  alias Spawn.Actors.Actor
  alias Spawn.Actors.ProjectionSettings
  alias Spawn.Actors.ProjectionSubject

  alias Google.Protobuf.Timestamp

  alias Spawn.Utils.Nats
  alias Gnat.Jetstream.API.Stream, as: NatsStream
  alias Gnat.Jetstream.API.Consumer

  @consumer_not_found_code 10014
  @one_day_in_ms :timer.hours(24)
  @stream_not_found_code 10059

  @spec init_projection_stream(actor :: Actor.t()) :: :ignore | {:error, any()} | {:ok, pid()}
  def init_projection_stream(%Actor{} = actor) do
    name = stream_name(actor)

    with {:create_stream, :ok} <- {:create_stream, create_stream(actor, true)},
         {:create_consumer, :ok} <-
           {:create_consumer, create_consumer(actor, deliver_policy: :all)} do
      start_pipeline(actor)
    else
      {:create_stream, error} ->
        Logger.error(
          "Error on start Projection #{name}. During phase [create_stream]. Details: #{inspect(error)}"
        )

        {:error, error}

      {:create_consumer, error} ->
        Logger.error(
          "Error on start Projection #{name}. During phase [create_consumer]. Details: #{inspect(error)}"
        )

        {:error, error}
    end
  end

  def init_sourceable_stream(%Actor{} = actor), do: create_stream(actor, false)

  def replay(stream_pid, actor, call_opts) do
    name = stream_name(actor)

    with {:stop_pipeline, :ok} <- {:stop_pipeline, stop_pipeline(stream_pid)},
         {:destroy_consumer, :ok} <- {:destroy_consumer, destroy_consumer(actor)},
         {:recreate_consumer, :ok} <- {:recreate_consumer, create_consumer(actor, call_opts)},
         {:start_pipeline, {:ok, newpid}} <- {:start_pipeline, start_pipeline(actor)} do
      {:ok, newpid}
    else
      {:stop_pipeline, error} ->
        Logger.error(
          "Error on start Projection #{name}. During phase [stop_pipeline]. Details: #{inspect(error)}"
        )

        {:error, error}

      {:destroy_consumer, error} ->
        Logger.error(
          "Error on start Projection #{name}. During phase [destroy_consumer]. Details: #{inspect(error)}"
        )

        {:error, error}

      {:recreate_consumer, error} ->
        Logger.error(
          "Error on start Projection #{name}. During phase [recreate_consumer]. Details: #{inspect(error)}"
        )

        {:error, error}

      {:start_pipeline, error} ->
        Logger.error(
          "Error on start Projection #{name}. During phase [start_pipeline]. Details: #{inspect(error)}"
        )

        {:error, error}
    end
  end

  defp build_consumer(stream_name, consumer_name, opts) do
    deliver_policy = Keyword.get(opts, :deliver_policy, :all)
    build_consumer_by_deliver(deliver_policy, stream_name, consumer_name, opts)
  end

  defp build_consumer_by_deliver(:by_start_time, stream_name, consumer_name, opts) do
    ten_minutes =
      DateTime.utc_now()
      |> DateTime.add(-@one_day_in_ms, :second)

    start_time = Keyword.get(opts, :opt_start_time, ten_minutes)

    %Consumer{
      stream_name: stream_name,
      durable_name: consumer_name,
      deliver_policy: :by_start_time,
      opt_start_time: start_time
    }
  end

  defp build_consumer_by_deliver(:all, stream_name, consumer_name, _opts) do
    %Consumer{stream_name: stream_name, durable_name: consumer_name, deliver_policy: :all}
  end

  defp build_sources(actor, %ProjectionSettings{} = settings) do
    settings.subjects
    |> Enum.map(fn %ProjectionSubject{} = subject ->
      opt_start_time =
        case subject.start_time do
          nil ->
            DateTime.from_unix!(0, :second)

          %Timestamp{seconds: start_at} ->
            DateTime.from_unix!(start_at, :second)
        end

      stream_name = stream_name(actor, subject.actor)

      %{
        name: stream_name,
        filter_subject: "actors.#{stream_name}.*.#{subject.source_action}",
        opt_start_time: opt_start_time
      }
    end)
  end

  defp build_stream_max_age(%ProjectionSettings{} = settings) do
    case Map.get(
           settings.events_retention_strategy || %{},
           :strategy,
           {:duration_ms, @one_day_in_ms}
         ) do
      {:infinite, true} -> 0
      # ms to ns
      {:duration_ms, max_age} -> max_age * 1_000_000
    end
  end

  defp conn, do: Nats.connection_name()

  defp create_stream(actor, true) do
    stream_name = stream_name(actor)
    max_age = build_stream_max_age(actor.settings.projection_settings)

    stream =
      %NatsStream{
        name: stream_name,
        subjects: [],
        sources: build_sources(actor, actor.settings.projection_settings),
        duplicate_window: max_age,
        max_age: max_age
      }

    case NatsStream.info(conn(), stream_name) do
      {:ok, _info} ->
        {:ok, _updated} = NatsStream.update(conn(), stream)

        :ok

      {:error, %{"code" => 404, "err_code" => @stream_not_found_code}} ->
        {:ok, %{created: _}} = NatsStream.create(conn(), stream)
        :ok

      error ->
        error
    end
  end

  defp create_stream(actor, false) do
    stream_name = stream_name(actor)
    max_age = build_stream_max_age(actor.settings.projection_settings)

    stream =
      %NatsStream{
        name: stream_name,
        subjects: ["actors.#{stream_name}.>"],
        max_age: max_age,
        duplicate_window: max_age
      }

    case NatsStream.info(conn(), stream_name) do
      {:ok, _info} ->
        :ok

      {:error, %{"code" => 404, "err_code" => @stream_not_found_code}} ->
        {:ok, %{created: _}} = NatsStream.create(conn(), stream)
        :ok

      error ->
        error
    end
  end

  defp create_consumer(actor, opts) do
    stream_name = stream_name(actor)
    consumer_name = stream_name(actor)

    case Consumer.info(conn(), stream_name, consumer_name) do
      {:ok, _info} ->
        :ok

      {:error,
       %{
         "code" => 404,
         "description" => "consumer not found",
         "err_code" => @consumer_not_found_code
       }} ->
        {:ok, %{created: _}} =
          Consumer.create(conn(), build_consumer(stream_name, consumer_name, opts))

        :ok

      error ->
        error
    end
  end

  defp destroy_consumer(actor) do
    stream_name = stream_name(actor)
    consumer_name = stream_name(actor)

    case Consumer.info(conn(), stream_name, consumer_name) do
      {:ok, _info} ->
        Consumer.delete(conn(), stream_name, consumer_name)

      {:error,
       %{
         "code" => 404,
         "description" => "consumer not found",
         "err_code" => @consumer_not_found_code
       }} ->
        :ok

      error ->
        error
    end
  end

  defp start_pipeline(actor) do
    StreamConsumer.start_link(%{
      actor_name: stream_name(actor),
      projection_pid: self(),
      strict_ordering: actor.settings.projection_settings.strict_events_ordering
    })
  end

  defp stop_pipeline(pid), do: Broadway.stop(pid)

  def stream_name(actor, actor_name \\ nil)
  def stream_name(%Actor{} = actor, actor_name), do: stream_name(actor.id, actor_name)

  def stream_name(actor_id, actor_name) do
    actor_name =
      actor_name ||
        if is_nil(actor_id.parent) or actor_id.parent == "",
          do: actor_id.name,
          else: actor_id.parent

    String.replace("#{actor_id.system}-#{actor_name}", ".", "-")
  end
end
