defmodule Clip do
  @moduledoc """
  Access the DB, find the segments to use,
  report gaps, and create the output file.
  """

  alias Ytex.SegPlVod
  alias Ytex.SegPlLive

  alias Db.Repo
  alias SegmentsDir

  alias __MODULE__.Ffmpeg
  alias Schemas.Clip.{Segment, Video}

  @type ss_to :: {number(), number()}
  @type channel :: :video | :audio
  @type type :: :live | :post_live | :vod

  @opaque cut_info :: %{
            segments: %{channel() => [String.t()]},
            o_ss: number(),
            t: number()
          }

  @doc """
  The `cut_info` and the gaps. Doesn't run the ffmpeg yet.
  """
  @spec get_cut_info(term(), ss_to(), type(), [channel()]) :: %{
          cut_info: cut_info() | nil,
          gaps: %{channel => [ss_to()]}
        }
  def get_cut_info(video_id, ss_to, type, channels)

  def get_cut_info(video_id, {ss, to} = ss_to, type, channels) do
    # Get all segments that have an intersection
    channels_segments = Segment.get_segments_in_intv(video_id, ss_to, type, channels)

    # Exact gap intervals per media chn
    # For :video and :audio
    gaps =
      for chn <- channels, into: %{} do
        segments = channels_segments[chn] || []

        # The gaps are ss_to \ segments
        gap_intvs = RSet.diff([%{ss: ss, to: to}], segments)
        {chn, gap_intvs}
      end

    # Get the very beginning and end of fetched segments, per media chn
    chn_ss_first_to_last =
      for {chn, [%Segment{ss: ss} | _] = segments} <- channels_segments, into: %{} do
        %Segment{to: to} = List.last(segments)
        {chn, {ss, to}}
      end

    cut_info =
      if Enum.all?(channels, &is_map_key(chn_ss_first_to_last, &1)) do
        # Do segment breadths per media chn intersect
        ss_intrs =
          Enum.map(chn_ss_first_to_last, fn {_, {ss, _}} -> ss end) |> Enum.max()

        to_intrs =
          Enum.map(chn_ss_first_to_last, fn {_, {_, to}} -> to end) |> Enum.max()

        # ss is best aligned at segment boundary
        ss_final = ss_intrs
        to_final = min(to, to_intrs)

        if to_final > ss_final do
          %{segments: channels_segments, o_ss: ss_final, t: to_final - ss_final}
        end
      else
        nil
      end

    %{cut_info: cut_info, gaps: gaps}
  end

  @doc """
  Creates the file with ffmpeg from `cut_info`.
  """
  @spec cut_ss(cut_info, String.t()) :: {:ok, size :: pos_integer()} | {:error, any()}
  def cut_ss(cut_info, output) do
    Ffmpeg.concat(cut_info.segments, cut_info.o_ss, cut_info.t, output)
  end

  def fetch_pl_urls(%Video{url: url}) do
    with {:ok, pl_urls, infodict} <- Ytex.url_to_plurl_infodict(url) do
      Video.upsert_from_infodict!(infodict)
      pl_urls
    end
  end

  def download_segments(gaps, media_chn, pl_url, %Video{start_time: start_time} = video) do
    result =
      for gap <- gaps do
        Ytex.plurl_to_segpl(pl_url, start_time)
        |> case do
          {:ok, %SegPlVod{} = seg_pl} ->
            SegPlVod.download_segments(
              seg_pl,
              gap,
              fn _ -> SegmentsDir.path(SegmentsDir) end,
              &insert_segment(:vod, &1, &2, media_chn, video)
            )

          {:ok, %SegPlLive{}} ->
            {:error, :cant_download_live}

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

    errors = Enum.reject(result, &(&1 == :ok))

    case errors do
      [] -> :ok
      errors -> {:error, errors}
    end
  end

  defp insert_segment(
         status,
         %Ytex.Segment{seq_no: seq_no, ss: ss, to: to},
         path,
         chn,
         video
       ) do
    segment =
      Ecto.build_assoc(video, :segments,
        type: status,
        channel: chn,
        ss: ss,
        to: to,
        path: path,
        seq_no: seq_no
      )

    Repo.insert!(segment)
  end
end
