defmodule Watcher.Persistent do
  @doc false
  # Watch a channel

  require Logger
  alias Db.Repo
  alias Ytex
  alias Ytex.{Infodict, SegPlLive}

  alias Schemas.Clip.{Channel, Video}
  alias SegmentsDir
  import Enum
  import Map, except: [map: 2]

  use GenServer, restart: :transient

  def start_link(%{url: _url, callbacks: _callbacks, task_sv: _task_sv} = opts) do
    GenServer.start_link(__MODULE__, opts, [])
  end

  def url_canon?(url) do
    Channel.canon?(url)
  end

  defp register_self_url(url) do
    Registry.register(Watcher.Registry, url, nil)
    |> case do
      {:ok, _} -> :ok
      {:error, {:already_registered, _pid}} -> :duplicate
    end
  end

  def stop(server) do
    GenServer.stop(server, 10_000)
  end

  @impl true
  def init(opts) do
    if url_canon?(opts.url) do
      case register_self_url(opts.url) do
        :ok ->
          ref = make_ref()
          Process.send_after(self(), {ref, :poll_time}, 0)
          Logger.notice(started_watching: opts.url)
          {:ok, {:not_live, put(opts, :timer_ref, ref)}}

        :duplicate ->
          {:stop, :duplicate}
      end
    else
      {:stop, :url_not_canon}
    end
  end

  @impl true
  def handle_info({timer_ref, :poll_time}, {:not_live, %{timer_ref: timer_ref} = state}) do
    case poll(state) do
      {:ok, pl_urls, infodict, vid, was_live?} ->
        if not was_live? do
          Logger.debug("Calling stream start callback.")
          state.callbacks.start.(vid, infodict)
        else
          Logger.debug("Stream was known to be live, no start callback.")
        end

        # Tasks will return :stop | :retries_failed
        dl_tasks = download_and_retry_tasks(pl_urls, vid, infodict, state.task_sv, 10)
        state = put(state, :dl_tasks, dl_tasks) |> delete(:timer_ref)
        {:noreply, {:live, state}}

      _ ->
        ref = make_ref()
        Process.send_after(self(), {ref, :poll_time}, state[:poll_interval] || 60_000)
        state = put(state, :timer_ref, ref)
        {:noreply, {:not_live, state}}
    end
  end

  def handle_info({ref, {_, vid}}, {:live, state})
      when is_reference(ref) and is_map_key(state.dl_tasks, ref) do
    %{
      true: [%{ref: ^ref}],
      false: other_tasks
    } = group_by(state.dl_tasks, fn task -> task.ref == ref end)

    _ = Process.demonitor(ref, [:flush])

    each(other_tasks, fn task ->
      _ = Process.demonitor(task.ref, [:flush])
      _ = Task.shutdown(task)
    end)

    state.callbacks[:stop] && state.callbacks[:stop].(vid)

    ref = make_ref()
    _ = Process.send_after(self(), {ref, :poll_time}, state.poll_interval)
    state = put(state, :timer_ref, ref) |> delete(:dl_tasks)
    {:noreply, {:not_live, state}}
  end

  defp poll(state) do
    Logger.debug(poll: :start, url: state.url)

    case Ytex.url_to_plurl_infodict(state.url <> "/live") do
      {:ok, pl_urls, %Infodict{status: :live, url: vid_url} = infodict} ->
        known_was_live? = known_live?(vid_url)
        video = Video.upsert_from_infodict!(infodict)
        Logger.info(poll: :stop, url: state.url)
        {:ok, pl_urls, infodict, video, known_was_live?}

      # Can't be not live
      # {:ok, _pl_urls, %Infodict{status: status, url: vid_url}} ->
      #   Logger.debug(poll: nil, url: state.url, status: status, vid_url: vid_url)
      #   false

      {:error, {:ytdl_error, reasons} = r} ->
        if reasons[:stderr] && parse_stderr(reasons[:stderr]) == :not_live do
          Logger.debug(poll: nil, url: state.url)
          false
        else
          if cookie_error?(reasons[:stderr]) do
            Logger.debug(poll: :cookie_error)
            rm = File.rm(".cookies.txt")
            true = rm in [:ok, {:error, :enoent}]
            false
          else
            Logger.error(poll: :error, url: state.url, reason: r)
            :error
          end
        end

      {:error, reason} ->
        Logger.error(poll: :error, url: state.url, reason: reason)
        :error
    end
  end

  # hack-y
  defp parse_stderr({_, stderr}) do
    if stderr =~ "not currently live" or
         stderr =~ "This live event will begin in" do
      :not_live
    else
      :error
    end
  end

  defp cookie_error?({_, stderr}) do
    stderr =~ "cookies"
  end

  import Ecto.Query

  defp known_live?(vid_url) do
    q = from v in Video, where: v.url == ^vid_url

    case Repo.one(q) do
      nil -> false
      %Video{status: :live} -> true
      %Video{status: _} -> false
    end
  end

  defp download_and_retry_tasks(pl_urls, vid, infodict, task_sv, retries) do
    Logger.notice(downloads: :start)

    Enum.map(pl_urls, fn
      {media_chn, pl_url} ->
        Task.Supervisor.async(task_sv, fn ->
          # :stop | :retries_failed
          download_and_retry(pl_url, media_chn, vid, infodict, retries)
        end)
    end)
  end

  def download_and_retry(_plurl, _chn, vid, _infodict, 0) do
    Logger.error(download: :retries_failed, vid: vid)
    {:retries_failed, vid}
  end

  def download_and_retry(
        plurl,
        chn,
        video,
        %Infodict{status: :live} = infodict,
        retries
      )
      when retries > 0 do
    {:ok, %SegPlLive{} = pl} =
      Ytex.plurl_to_segpl(Ytex.base_request(), plurl, infodict.start_time)

    case do_download(pl, chn, video) do
      :stop ->
        Logger.info(download: :stopped, vid: video)
        {:stop, video}

      {:error, _} ->
        download_and_retry(plurl, chn, video, infodict, retries - 1)
    end
  end

  defp do_download(pl, chn, video) do
    path = SegmentsDir.path(SegmentsDir)
    file_coll = File.stream!(path)

    case SegPlLive.next_segment(pl, Ytex.base_request(), file_coll) do
      {:continue, pl, %Ytex.Segment{} = segment, _} ->
        SegmentsDir.insert_segment(segment, path, chn, video)
        do_download(pl, chn, video)

      :stop ->
        :stop

      {:error, err} ->
        {:error, err}
    end
  end
end
