defmodule Bot.Clip do
  @doc false
  # The "clip" command

  alias Schemas.Bot.ClipMsg
  alias Web
  alias ClipDir
  alias SegmentsDir
  alias Bot.Cache
  alias Db.Repo
  alias Req.Response
  alias Bot.HTTPApiRateLimiterSupervisor, as: RateLimiter
  alias Schemas.Bot.Registration
  alias Schemas.Clip.Channel
  alias Schemas.Clip.Video
  alias Pana.HTTPApi
  alias Ytex.Infodict
  alias Ater.Command
  alias Ater.Command.Option
  alias Ater.Enum.OptionType
  import Ater.EventHandler, only: [text_followup_response: 1, text_followup_response: 2]
  import Ecto.Query
  require Logger

  @max_dur 300
  @default_dur 10

  @clip %Command{
    description: "Clip a timestamp",
    fun: {__MODULE__, :clip},
    name: "c-timestamp",
    options: [
      %Option{
        name: "timestamp",
        description: "eg. 1:05:03 or 3903",
        required: true,
        type: %OptionType{val: :string}
      },
      %Option{
        name: "duration",
        description: "Duration, 10 seconds by default",
        required: false,
        type: %OptionType{val: :integer},
        min_value: 1,
        max_value: @max_dur
      },
      %Option{
        name: "stream",
        description: "Url of the stream",
        required: false,
        type: %OptionType{val: :string}
        # autocomplete: true
      }
    ],
    send_thinking: true,
    reply_callback: {__MODULE__, :clip_reply_callback}
  }

  @spec clip(%{String.t() => map()}, map(), term()) :: {result_type, result}
        when result_type:
               :response
               | {:file_response, file_resp :: term()}
               | {:file_stream_response, file_stream_resp :: term()}
               | :ignore,
             result: term()

  def clip(opts, interaction, state) do
    dc_channel_id = interaction["channel_id"] |> to_string()

    video_result = opts["stream"]["value"] |> video_from_url(dc_channel_id)
    timestamp_result = opts["timestamp"]["value"] |> parse_timestamp
    duration_result = (opts["duration"]["value"] || @default_dur) |> validate_dur

    validate_and_do_clip(
      interaction,
      video_result,
      timestamp_result,
      duration_result,
      state
    )
  end

  @clip_sseof %Command{
    description: "Clip the latest part of a live-stream",
    fun: {__MODULE__, :clip_sseof},
    name: "c",
    options: [
      %Option{
        name: "seconds_ago",
        description: "By default last 10 seconds",
        required: false,
        min_value: 1,
        type: %OptionType{val: :integer}
      },
      %Option{
        name: "duration",
        description: "Duration of the clip",
        required: false,
        type: %OptionType{val: :integer},
        min_value: 1,
        max_value: @max_dur
      }
    ],
    send_thinking: true,
    reply_callback: {__MODULE__, :clip_reply_callback}
  }

  @spec clip_sseof(%{String.t() => map()}, map(), term()) :: {result_type, result}
        when result_type:
               :response
               | {:file_response, file_resp :: term()}
               | {:file_stream_response, file_stream_resp :: term()}
               | :ignore,
             result: term()

  def clip_sseof(opts, interaction, state) do
    dc_channel_id = interaction["channel_id"] |> to_string()

    sseof = opts["seconds_ago"]["value"] || @default_dur
    duration = opts["duration"]["value"] || sseof
    sseof = sseof + 2
    true = is_number(sseof)
    true = is_number(duration)

    video_result = video_from_url(nil, dc_channel_id)
    duration_result = validate_dur(duration)

    timestamp_result =
      case video_result do
        {:ok, video, _, _} ->
          {:ok, (DateTime.utc_now() |> DateTime.diff(video.start_time)) - sseof}

        err ->
          err
      end

    validate_and_do_clip(
      interaction,
      video_result,
      timestamp_result,
      duration_result,
      state
    )
  end

  def clip_reply_callback(
        _og_intr,
        {:ok, %Req.Response{body: body}},
        %ClipMsg{} = partial_clip_msg
      ) do
    msg_id = body.id |> to_string()

    # belongs_to :video, Video
    ClipMsg.changeset(partial_clip_msg, discord_msg_id: msg_id)
    |> Repo.insert!()
  end

  def clip_reply_callback(_og_intr, {:ok, %Req.Response{body: body}}, nil) do
    nil
  end

  defp validate_and_do_clip(
         interaction,
         video_result,
         timestamp_result,
         duration_result,
         state
       ) do
    with {:ok, %Video{} = video, maybe_pl_urls, maybe_infodict} <- video_result,
         {:ok, timestamp} <- timestamp_result,
         {:ok, duration} <- duration_result do
      #
      max_upload_size = max_upload_size(interaction["guild"], state.app)
      # Do clip and get the output
      case do_clip(video, timestamp, duration, maybe_pl_urls || %{}, maybe_infodict) do
        {:ok, clip_result} ->
          response =
            if clip_result.size <= max_upload_size or max_upload_size == nil do
              clip_command_result(clip_result)
            else
              clip_too_big_result(clip_result)
            end

          # Pass to response callback
          clip_msg =
            %ClipMsg{
              ss: timestamp,
              t: duration,
              file_path: clip_result.filename,
              size: clip_result.size,
              video_id: video.id
            }

          {[callback_data: clip_msg], response}

        {:error, reason} ->
          clip_error_result(reason)
      end

      # If options fail to be parsed, return error responses
    else
      {:timestamp_error, timestamp} ->
        true = String.valid?(timestamp)

        text_followup_response("Timestamp format wrong: #{timestamp} 😕", [
          :no_mentions,
          :suppress_embeds
        ])

      {:duration_error, duration} when is_integer(duration) ->
        text_followup_response("Duration must be less than #{@max_dur} seconds 😕", [
          :no_mentions,
          :suppress_embeds
        ])

      {:stream_error, :not_valid_url, stream} ->
        text_followup_response("#{stream} is not valid 😕", [
          :no_mentions,
          :suppress_embeds
        ])

      {:stream_error, :no_stream, stream} ->
        text_followup_response("#{stream} is not valid 😕", [
          :no_mentions,
          :suppress_embeds
        ])

      {:stream_error, :no_default} ->
        text_followup_response("I don't know which stream to clip 😕")
    end
  end

  @spec do_clip(
          Video.t(),
          number(),
          number(),
          %{atom => String.t()},
          Infodict.t() | nil
        ) ::
          {:ok,
           %{
             file_stream: Enum.t(),
             filename: String.t(),
             size: pos_integer()
           }}
          | {:error, any()}
  defp do_clip(%Video{} = vid, timestamp, duration, pl_urls, infodict) do
    case (infodict && infodict.status) || vid.status do
      :live -> do_clip_live(vid, timestamp, duration)
      :postlive -> do_clip_live(vid, timestamp, duration)
      :vod -> do_clip_vod(vid, timestamp, duration, pl_urls, infodict)
    end
  end

  defp do_clip_vod(vid, ts, dur, pl_urls, infodict) do
    %{cut_info: live_cut_info, gaps: live_gaps} =
      Clip.get_cut_info(vid.id, {ts, ts + dur}, :live, [:video, :audio])

    %{cut_info: vod_cut_info, gaps: vod_gaps} =
      Clip.get_cut_info(vid.id, {ts, ts + dur}, :vod, [:video, :audio])

    if live_gaps == %{} and live_cut_info != nil do
      do_clip_vod_from_live(live_cut_info, vid.title, ts, dur)
    else
      do_clip_vod_from_vod(vod_cut_info, vod_gaps, vid, ts, dur, pl_urls, infodict)
    end
  end

  defp do_cut_result(cut_info, title, ts, dur) do
    output = clip_file_path(title, ts, dur)

    case Clip.cut_ss(cut_info, output) do
      {:ok, size} ->
        clip_result = %{
          file_stream: File.stream!(output, 4096),
          filename: output,
          size: size
        }

        {:ok, clip_result}

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

  defp do_clip_vod_from_vod(cut_info, chn_gaps, vid, ts, dur, _, _)
       when chn_gaps == %{} do
    do_cut_result(cut_info, vid.title, ts, dur)
  end

  defp do_clip_vod_from_vod(_cut_info, chn_gaps, vid, ts, dur, pl_urls, _)
       when is_map(pl_urls) do
    dl_result =
      for {media_chn, gaps} <- chn_gaps, reduce: [] do
        reasons ->
          gaps = gaps |> List.flatten() |> Enum.map(&{&1.ss, &1.to})

          pl_url =
            case pl_urls do
              %{^media_chn => pl_url} ->
                pl_url

              _ ->
                %{^media_chn => pl_url} = Clip.fetch_pl_urls(vid)
                pl_url
            end

          result = Clip.download_segments(gaps, media_chn, pl_url, vid)

          case result do
            :ok -> reasons
            {:error, reason} -> [{media_chn, reason} | reasons]
          end
      end
      |> case do
        [] -> :ok
        reasons -> {:error, reasons}
      end

    Clip.get_cut_info(vid.id, {ts, ts + dur}, :vod, [:video, :audio])
    |> case do
      # This should not happen:
      # %{cut_info: nil} when dl_result == :ok -> {:error, :cant_download}
      %{cut_info: nil} ->
        {:error, reason} = dl_result
        {:error, reason}

      %{cut_info: cut_info, gaps: _} ->
        do_cut_result(cut_info, vid.title, ts, dur)
    end
  end

  defp do_clip_vod_from_live(cut_info, title, ts, dur) do
    do_cut_result(cut_info, title, ts, dur)
  end

  defp do_clip_live(vid, timestamp, duration) do
    %{
      cut_info: cut_info,
      gaps: _gaps
    } =
      Clip.get_cut_info(vid.id, {timestamp, timestamp + duration}, :live, [
        :video,
        :audio
      ])

    case cut_info do
      cut_info when cut_info != nil ->
        do_cut_result(cut_info, vid.title, timestamp, duration)

      nil ->
        {:error, :no_live_segments}
    end
  end

  defp clip_file_path(title, timestamp, duration) do
    true = String.valid?(title)
    title = String.replace(title, ["/", " ", "?"], "_")
    name = "#{title}_#{timestamp}_#{duration}.mp4"
    dir = ClipDir.path(ClipDir) |> dbg()
    Path.join(dir, name)
  end

  defp clip_command_result(result) do
    payload = %{
      attachments: [
        %{id: 0}
      ]
    }

    file_stream = [
      %{
        id: 0,
        filename: result.filename |> Path.basename(),
        content_stream: result.file_stream,
        mime_type: "video/mp4"
      }
    ]

    {:file_stream_response, file_stream, payload}
  end

  defp clip_too_big_result(result) do
    route = Web.Clips.get_route(result.filename)

    payload = %{
      content: route
    }

    {:response, payload}
  end

  defp clip_error_result(error) do
    Logger.warning(clip_error: error)
    text_followup_response("Clipping failed 😖")
  end

  defp parse_timestamp(input) do
    parts = String.split(input, ":") |> Enum.map(&Integer.parse/1)

    case parts do
      [{seconds, ""}] ->
        {:ok, seconds}

      [{minutes, ""}, {seconds, ""}] ->
        {:ok, minutes * 60 + seconds}

      [{hours, ""}, {minutes, ""}, {seconds, ""}] ->
        {:ok, hours * 3600 + minutes * 60 + seconds}

      _ ->
        {:timestamp_error, input}
    end
  end

  defp validate_dur(dur) when is_integer(dur) do
    if dur in 1..@max_dur do
      {:ok, dur}
    else
      {:duration_error, dur}
    end
  end

  defp video_from_url(nil, dc_chn_id) do
    case latest_vid(dc_chn_id) do
      nil -> {:stream_error, :no_default}
      %Video{} = vid -> {:ok, vid, nil, nil}
    end
  end

  defp video_from_url(url, _dc_chn_id) do
    case Video.canon_url(url) do
      {:ok, cnn_url} ->
        case Video.from_cnnurl(cnn_url) do
          %Video{} = video -> {:ok, video, nil, nil}
          nil -> fetch_unknown_video(cnn_url)
        end

      {:error, :not_valid} ->
        {:stream_error, :not_valid_url, url}
    end
  end

  defp latest_vid(dc_chn_id) do
    q =
      from reg in Registration,
        where: reg.discord_chn_id == ^dc_chn_id,
        join: c in Channel,
        prefix: "clip",
        on: reg.channel_id == c.id,
        join: v in Video,
        prefix: "clip",
        on: v.channel_id == c.id,
        select: v,
        order_by: {:desc, v.start_time}

    Repo.all(q) |> List.first()
  end

  defp fetch_unknown_video(url) do
    case Ytex.url_to_plurl_infodict(url) do
      {:ok, pl_urls, %Infodict{} = infodict} ->
        video = Video.upsert_from_infodict!(infodict)
        {:ok, video, pl_urls, infodict}

      {:error, _err} ->
        {:stream_error, :no_stream, url}
    end
  end

  defp premium_max_upload_size(prem_tier) do
    case prem_tier do
      0 -> 25
      1 -> 25
      2 -> 50
      3 -> 100
    end * 1_000_000
  end

  defp max_upload_size(%{premium_tier: prem_tier} = guild, app) do
    max_upload_size(
      put_in(guild, ["premium_tier"], prem_tier) |> Map.delete(:premium_tier),
      app
    )
  end

  defp max_upload_size(%{id: id} = guild, app) do
    max_upload_size(put_in(guild, ["id"], id) |> Map.delete(:id), app)
  end

  defp max_upload_size(%{"premium_tier" => prem_tier, "id" => id} = _guild, _app) do
    max_size = premium_max_upload_size(prem_tier)
    :ok = insert_max_upload_size_to_cache(id, max_size)
    max_size
  end

  defp max_upload_size(%{"id" => id} = _guild, app) do
    id = to_string(id)

    case :ets.lookup(Cache, {:max_upload_size, id}) do
      [] -> fetch_guild_info(id, app) |> max_upload_size(app)
      [{{:max_upload_size, ^id}, max_size}] -> max_size
    end
  end

  defp insert_max_upload_size_to_cache(id, max_size) do
    id = to_string(id)
    true = :ets.insert(Cache, {{:max_upload_size, id}, max_size})
    :ok
  end

  defp fetch_guild_info(id, app) do
    ctx = %{rate_limiter: RateLimiter, app: app}

    %Response{status: 200, body: body} =
      HTTPApi.request(ctx, :get, "/guilds/#{id}", nil) |> Req.request!()

    body
  end

  def commands do
    [@clip, @clip_sseof]
  end
end
