defmodule Extreme.Listener do
  @moduledoc ~S"""
  TODO
  """
  require Logger

  defmacro __using__(opts \\ []) do
    quote location: :keep do
      use GenServer
      require Logger

      @exclude_catch_all_handle_info Keyword.get(
                                       unquote(opts),
                                       :exclude_catch_all_handle_info,
                                       false
                                     )

      @default_read_per_page 500

      def child_spec([extreme, stream_name | opts]) do
        %{
          id: __MODULE__,
          start: {__MODULE__, :start_link, [extreme, stream_name, opts]}
        }
      end

      @doc """
      Starts Listener GenServer with `extreme` connection, for particular `stream_name`
      and options:

      * `:name` of listener process. Defaults to module name.
      * `:read_per_page` - number of events read in batches until all existing evets are processed. Defaults to 500
      * `:resolve_link_tos` - weather to resolve event links. Defaults to true.
      * `:require_master` - check if events are expected from master ES node. Defaults to false.
      * `:ack_timeout` - Wait time for ack message. Defaults to 5_000 ms.
      """
      def start_link(extreme, stream_name, opts \\ []) do
        {read_per_page, opts} = Keyword.pop(opts, :read_per_page, @default_read_per_page)
        {resolve_link_tos, opts} = Keyword.pop(opts, :resolve_link_tos, true)
        {require_master, opts} = Keyword.pop(opts, :require_master, false)
        {ack_timeout, opts} = Keyword.pop(opts, :ack_timeout, 5_000)
        {auto_subscribe?, opts} = Keyword.pop(opts, :auto_subscribe?, true)
        opts = Keyword.put_new(opts, :name, __MODULE__)

        GenServer.start_link(
          __MODULE__,
          {extreme, stream_name, read_per_page, resolve_link_tos, require_master, ack_timeout,
           auto_subscribe?},
          opts
        )
      end

      def unsubscribe(server \\ __MODULE__), do: GenServer.call(server, :unsubscribe)

      @doc """
      Initializes subscription with EventStore if one one does not exist already.
      If it does, it's a noop.
      """
      def subscribe(server \\ __MODULE__), do: GenServer.cast(server, :subscribe)
      def subscribed?(server \\ __MODULE__), do: GenServer.call(server, :subscribed?)

      @impl true
      def init(
            {extreme, stream_name, read_per_page, resolve_link_tos, require_master, ack_timeout,
             auto_subscribe?}
          ) do
        state = %{
          extreme: extreme,
          subscription: nil,
          subscription_ref: nil,
          stream_name: stream_name,
          last_event: nil,
          mode: :init,
          per_page: read_per_page,
          resolve_link_tos: resolve_link_tos,
          require_master: require_master,
          ack_timeout: ack_timeout
        }

        :ok = on_init(state)

        if auto_subscribe?,
          do: GenServer.cast(self(), :subscribe)

        {:ok, state}
      end

      @impl true
      def handle_call({:on_event, _push}, _from, %{subscription: nil, mode: :live} = state),
        do: {:reply, :ok, state}

      def handle_call(
            {:on_event, push},
            _from,
            %{subscription: subscription, mode: :live} = state
          ) do
        push
        |> process_push(state.stream_name)
        |> case do
          {:ok, event_number} ->
            {:reply, :ok, %{state | last_event: event_number}}

          :stop ->
            true = Process.demonitor(state.subscription_ref)
            {:reply, :stop, %{state | subscription: nil, subscription_ref: nil}}
        end
      end

      def handle_call(:unsubscribe, _from, state),
        do: _unsubscribe(state)

      def handle_call(:subscribed?, _from, state),
        do: {:reply, !!state.subscription, state}

      defp _unsubscribe(%{subscription: nil, subscription_ref: nil} = state),
        do: {:reply, :ok, state}

      defp _unsubscribe(state) do
        Logger.info(
          "#{__MODULE__} unsubscribed from #{state.stream_name}. Last processed event: #{state.last_event}"
        )

        true = Process.demonitor(state.subscription_ref)

        :ok = state.extreme.unsubscribe(state.subscription)
        {:reply, :ok, %{state | subscription: nil, subscription_ref: nil}}
      end

      @impl true
      def handle_cast(:subscribe, state) do
        if state.subscription && Process.alive?(state.subscription) do
          Logger.warning("Ignoring subscribe attempt with existing subscriber.")
          {:noreply, state}
        else
          {:ok, state} =
            case get_last_event(state.stream_name) do
              #            :from_now ->
              #              {:ok, %{last_event_number: last_event}} =
              #                Extreme.execute(state.event_store, _read_events_backward(state.stream_name))
              #
              #              _start_subscription(last_event, state)
              #
              #            {:patch, last_event, patch_until} ->
              #              start_patching(last_event, patch_until, state)
              #
              last_event ->
                _start_subscription(last_event, state)
            end

          {:noreply, state}
        end
      end

      @impl true
      def handle_info({:DOWN, ref, :process, _pid, reason}, %{subscription_ref: ref} = state)
          when reason in [:pause, :done] do
        Logger.info("Subscription to stream #{state.stream_name} is #{inspect(reason)}")
        {:noreply, %{state | subscription: nil, subscription_ref: nil, mode: reason}}
      end

      def handle_info({:DOWN, ref, :process, _pid, _reason}, %{subscription_ref: ref} = state) do
        reconnect_delay = 1_000
        Logger.warning("Subscription to EventStore is down. Will retry in #{reconnect_delay} ms.")
        :timer.sleep(reconnect_delay)
        GenServer.cast(self(), :subscribe)
        {:noreply, state}
      end

      def handle_info(:caught_up, %{subscription: subscription} = state)
          when not is_nil(subscription) do
        caught_up()
        {:noreply, state}
      end

      unless @exclude_catch_all_handle_info do
        def handle_info(_msg, state), do: {:noreply, state}
      end

      defp _start_subscription(last_event, state) do
        {:ok, subscription} =
          state.extreme.read_and_stay_subscribed(
            state.stream_name,
            self(),
            last_event + 1,
            state.per_page,
            state.resolve_link_tos,
            state.require_master,
            state.ack_timeout
          )

        ref = Process.monitor(subscription)

        Logger.info(fn ->
          "#{__MODULE__} subscribed to stream #{state.stream_name}. Start processing live events from event no: #{last_event + 1}"
        end)

        {:ok,
         %{
           state
           | subscription: subscription,
             subscription_ref: ref,
             last_event: last_event,
             mode: :live
         }}
      end

      defp _read_events_backward(stream, start \\ -1, count \\ 1) do
        Extreme.Messages.ReadStreamEventsBackward.new(
          event_stream_id: stream,
          from_event_number: start,
          max_count: count,
          resolve_link_tos: true,
          require_master: false
        )
      end

      def caught_up, do: Logger.debug(fn -> "We are up to date" end)
      def on_init(_), do: Logger.info(fn -> "#{__MODULE__} started" end)
      def register_patching_start(_, _, _), do: {:error, :not_implemented}
      def patching_done(_), do: {:error, :not_implemented}
      def process_patch(_, _), do: {:error, :not_implemented}

      defoverridable caught_up: 0,
                     on_init: 1,
                     register_patching_start: 3,
                     patching_done: 1,
                     process_patch: 2
    end
  end
end
