defmodule ChnRegistry.Manager do
  @moduledoc """
  Documentation for `ChnRegistry`.
  """
  require Logger
  alias Schemas.Clip.Video
  alias Db.Repo
  alias Schemas.Clip.Channel
  alias Schemas.Bot.Registration

  import Ecto.Query

  @type dc_callbacks :: %{
          start: (dc_chn_id :: String.t(), Channel.t(), Video.t() -> any),
          stop: (dc_chn_id :: String.t(), Channel.t(), Video.t() -> any) | nil
        }

  @spec add_registry(String.t(), String.t(), term()) :: :ok | {:error, :bad_url}
  def add_registry(dc_chn_id, url, callbacks) do
    case Channel.url_to_username(url) do
      nil ->
        {:error, :bad_url}

      username ->
        yt_chn_result =
          from(c in Channel, where: [url: ^url, username: ^username])
          |> Repo.one()
          |> case do
            nil ->
              Channel.changeset(%Channel{}, url: url, username: username)
              |> Repo.insert(returning: true)
              |> case do
                {:ok, chn} -> {:ok, chn}
                {:error, _err} -> {:error, :bad_url}
              end

            chn ->
              {:ok, chn}
          end

        with {:ok, yt_chn} <- yt_chn_result do
          Registration.changeset(%Registration{},
            discord_chn_id: dc_chn_id,
            channel_id: yt_chn.id
          )
          |> Repo.insert!(on_conflict: :nothing)

          GenServer.call(
            __MODULE__,
            {:start_watcher, dc_chn_id, yt_chn, callbacks}
          )
        end
    end
  end

  def remove_registry(dc_chn_id, url) do
    case Channel.url_to_username(url) do
      nil ->
        {:error, :bad_url}

      username ->
        yt_chn_result =
          from(c in Channel, where: [url: ^url, username: ^username])
          |> Repo.one()
          |> case do
            nil ->
              Channel.changeset(%Channel{}, url: url, username: username)
              |> Repo.insert(returning: true)
              |> case do
                {:ok, chn} -> {:ok, chn}
                {:error, _err} -> {:error, :bad_url}
              end

            chn ->
              {:ok, chn}
          end

        with {:ok, yt_chn} <- yt_chn_result do
          Registration.changeset(%Registration{},
            discord_chn_id: dc_chn_id,
            channel_id: yt_chn.id
          )

          q =
            from r in Registration,
              where: [discord_chn_id: ^dc_chn_id, channel_id: ^yt_chn.id]

          {deleted_count, _} = Repo.delete_all(q)

          :ok =
            GenServer.call(
              __MODULE__,
              {:stop_watcher, dc_chn_id, yt_chn}
            )

          Logger.notice(watcher_stop: :ok)

          case deleted_count do
            0 -> {:error, :nokey}
            1 -> :ok
          end
        end
    end
  end

  def start_link(init_callbacks) do
    GenServer.start_link(__MODULE__, init_callbacks, name: __MODULE__)
  end

  use GenServer, restart: :permanent

  import Map

  @type state :: %{
          (yt_chn_id :: integer()) => %{
            url: String.t(),
            dc_chns: %{
              (dc_chn_id :: String.t()) => dc_callbacks
            }
          }
        }

  @impl true
  def init(init_callbacks) do
    true = Process.link(Process.whereis(Watcher.Persistent.Supervisor))
    {:ok, %{}, {:continue, {:start_from_db, init_callbacks}}}
  end

  @impl true
  def handle_continue({:start_from_db, init_callbacks}, state) do
    all_in_db = all_in_db()

    state =
      Enum.reduce(
        all_in_db,
        state,
        fn
          %Registration{channel: yt_chn, discord_chn_id: dc_chn_id}, state ->
            do_start_watcher(state, yt_chn, dc_chn_id, init_callbacks)
        end
      )

    {:noreply, state}
  end

  defp all_in_db do
    Repo.all(from r in Registration, preload: [:channel])
  end

  defp notify_me_start_cb(vid, infodict) do
    GenServer.cast(__MODULE__, {:watcher_vid_start, vid, infodict})
  end

  defp notify_me_stop_cb(vid) do
    GenServer.cast(__MODULE__, {:watcher_vid_stop, vid})
  end

  defp do_start_watcher(state, yt_chn, dc_chn_id, callbacks) do
    yt_chn_id = yt_chn.id

    notify_callbacks = %{
      start: &notify_me_start_cb/2,
      stop: &notify_me_stop_cb/1
    }

    cond do
      not has_key?(state, yt_chn_id) ->
        {:ok, _watcher_pid} = Watcher.start_watching(yt_chn.url, notify_callbacks)
        put(state, yt_chn_id, %{dc_chns: %{dc_chn_id => callbacks}})

      Watcher.pid_from_url(yt_chn.url) == nil ->
        {:ok, _watcher_pid} = Watcher.start_watching(yt_chn.url, notify_callbacks)
        put_in(state, [yt_chn_id, :dc_chns, dc_chn_id], callbacks)

      Watcher.pid_from_url(yt_chn.url) != nil ->
        put_in(state, [yt_chn_id, :dc_chns, dc_chn_id], callbacks)
    end
    |> put_in([yt_chn_id, :url], yt_chn.url)
  end

  defp do_stop_watcher(state, yt_chn, dc_chn_id) do
    yt_chn_id = yt_chn.id

    if has_key?(state, yt_chn_id) do
      state = update_in(state, [yt_chn_id, :dc_chns], &delete(&1, dc_chn_id))

      stop_if_non_left(state, yt_chn_id)
    else
      state
    end
  end

  defp stop_if_non_left(state, yt_chn_id) do
    if state[yt_chn_id][:dc_chns] == %{} do
      watcher_pid = Watcher.pid_from_url(state[yt_chn_id].url)
      _ = DynamicSupervisor.terminate_child(Watcher.Persistent.Supervisor, watcher_pid)

      state
    else
      state
    end
  end

  @impl true
  def handle_call({:start_watcher, dc_chn_id, yt_chn, callbacks}, _, state) do
    state = do_start_watcher(state, yt_chn, dc_chn_id, callbacks)

    {:reply, :ok, state}
  end

  def handle_call({:stop_watcher, dc_chn_id, yt_chn}, _, state) do
    state = do_stop_watcher(state, yt_chn, dc_chn_id)
    {:reply, :ok, state}
  end

  @impl true
  def handle_cast(
        {:watcher_vid_start, %Video{channel_id: yt_chn_id} = vid, _infodict},
        state
      ) do
    # Bad blocking code, w/e

    vid = Repo.preload(vid, :channel)

    Enum.each(
      state[yt_chn_id].dc_chns,
      fn {dc_chn_id, callbacks} ->
        callbacks[:start] && callbacks[:start].(dc_chn_id, vid.channel, vid)
      end
    )

    {:noreply, state}
  end

  def handle_cast({:watcher_vid_stop, %Video{channel_id: yt_chn_id} = vid}, _, state) do
    vid = Repo.preload(vid, :channel)

    Enum.each(
      state[yt_chn_id].dc_chns,
      fn {dc_chn_id, callbacks} ->
        callbacks[:stop] && callbacks[:stop].(dc_chn_id, vid.channel, vid)
      end
    )

    {:noreply, state}
  end

  @impl true
  def handle_info({:DOWN, _ref, :process, _object, _reason}, state) do
    {:noreply, state}
  end
end
