defmodule Bot.Register do
  @doc false
  # The "register" command

  require Logger
  alias Schemas.Clip.Video
  alias Req.Response
  alias Pana.HTTPApi
  alias Ater.Enum.IntrCbType
  alias Db.Repo
  alias Schemas.Bot.Registration
  alias Schemas.Clip.Channel
  alias Ater.Command
  alias Ater.Command.Option
  alias Ater.Enum.OptionType

  import Bitwise

  @suppress_embeds 1 <<< 2

  @register %Command{
    description: "Enable clipping a Youtube channel",
    fun: {__MODULE__, :register},
    name: "register",
    options: [
      %Option{
        name: "channel",
        description: "eg. \"https://www.youtube.com/@...\" ",
        required: false,
        type: %OptionType{val: :string}
      }
    ],
    send_thinking: false
  }

  @spec register(%{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 register(%{"channel" => %{"value" => url}}, interaction, state)
      when is_binary(url) do
    dc_channel_id = interaction["channel_id"] |> to_string()
    url = String.downcase(url)

    watcher_callbacks = watcher_callbacks(state.app, state.rate_limiter)

    registry_result = ChnRegistry.add_registry(dc_channel_id, url, watcher_callbacks)

    case registry_result do
      :ok ->
        # either success or noop on duplicate
        list_registered_response(dc_channel_id)

      {:error, :bad_url} ->
        bad_url_response(url)
    end
  rescue
    # Unhandled exception, so a bug.
    # Since the command had sent a "thinking" response first,
    # not sending a followup will cause a infinite loading
    # state on the UI. Send something to avoid that.
    exc ->
      Logger.error(Exception.format(:error, exc, __STACKTRACE__))
      {:response, %{content: "Bot broke :("}}
  end

  def register(opts, interaction, _state) when opts === %{} do
    dc_channel_id = interaction["channel_id"] |> to_string()
    list_registered_response(dc_channel_id)
  rescue
    # Unhandled exception, so a bug.
    # Since the command had sent a "thinking" response first,
    # not sending a followup will cause a infinite loading
    # state on the UI. Send something to avoid that.
    exc ->
      Logger.error(Exception.format(:error, exc, __STACKTRACE__))
      {:response, %{content: "Bot broke :("}}
  end

  def watcher_callbacks(app, rate_limiter) do
    http_api_ctx = %{app: app, rate_limiter: rate_limiter}

    %{
      start: fn dc_chn_id, chn, vid ->
        notify_stream_start(vid, chn, http_api_ctx, dc_chn_id)
      end,
      stop: fn _ -> :ok end
    }
  end

  @unregister %Command{
    description: "Undo the /register command",
    fun: {__MODULE__, :unregister},
    name: "unregister",
    options: [
      %Option{
        name: "channel",
        description: "eg. \"https://www.youtube.com/@...\" ",
        required: true,
        type: %OptionType{val: :string}
      }
    ],
    send_thinking: false
  }
  @spec unregister(%{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 unregister(%{"channel" => %{"value" => url}}, interaction, _state)
      when is_binary(url) do
    url = String.downcase(url)
    dc_channel_id = interaction["channel_id"] |> to_string()

    _ = ChnRegistry.remove_registry(dc_channel_id, url)

    list_registered_response(dc_channel_id)
  rescue
    # Unhandled exception, so a bug.
    # Since the command had sent a "thinking" response first,
    # not sending a followup will cause a infinite loading
    # state on the UI. Send something to avoid that.
    exc ->
      Logger.error(Exception.format(:error, exc, __STACKTRACE__))

      {:response,
       %{
         type: %IntrCbType{val: :channel_message_with_source},
         data: %{
           content: "Bot broke :(",
           flags: @suppress_embeds,
           allowed_mentions: %{parse: []}
         }
       }}
  end

  defp bad_url_response(url) do
    {:response,
     %{
       type: %IntrCbType{val: :channel_message_with_source},
       data: %{
         content: "#{url} is not a valid url :/",
         flags: @suppress_embeds,
         allowed_mentions: %{parse: []}
       }
     }}
  end

  def notify_stream_start(%Video{title: title, url: url}, _chn, http_api_ctx, dc_chn_id) do
    content = %{
      content: "🔴 Clipping enabled for #{title} (#{url})",
      flags: @suppress_embeds,
      allowed_mentions: %{parse: []}
    }

    HTTPApi.request(http_api_ctx, :post, "/channels/#{dc_chn_id}/messages", content)
    |> Req.request!()
    |> case do
      %Response{status: 403} ->
        unregister_if_channel_inaccesible(http_api_ctx, dc_chn_id)

      %Response{status: status} when status in 200..299 ->
        :ok
    end
  end

  defp unregister_if_channel_inaccesible(http_api_ctx, dc_chn_id) do
    HTTPApi.request(http_api_ctx, :get, "/channels/#{dc_chn_id}", nil)
    |> Req.request!()
    |> case do
      %Response{status: 200} ->
        :ok

      %Response{status: status} when status in [403, 404] ->
        yt_channels = yt_chns_from_dc_chn(dc_chn_id)
        Logger.notice(unaccessable: dc_chn_id, unregister: yt_channels)
        remove_registered(dc_chn_id, yt_channels)
    end
  end

  defp list_registered_response(dc_channel_id) when is_binary(dc_channel_id) do
    yt_channels = yt_chns_from_dc_chn(dc_channel_id)

    list =
      yt_channels
      |> Enum.map(fn %Channel{url: url, username: username} ->
        "* #{username} (#{url})"
      end)
      |> Enum.join("\n")
      |> case do
        "" -> "None"
        other -> other
      end

    {:response,
     %{
       type: %IntrCbType{val: :channel_message_with_source},
       data: %{
         content: "Youtube channels registered in <##{dc_channel_id}>:\n" <> list,
         flags: @suppress_embeds,
         allowed_mentions: %{parse: []}
       }
     }}
  end

  import Ecto.Query

  def yt_chns_from_dc_chn(dc_chn_id) do
    from(r in Registration,
      where: r.discord_chn_id == ^dc_chn_id,
      join: c in assoc(r, :channel),
      prefix: "clip",
      select: c
    )
    |> Repo.all()
  end

  def remove_registered(dc_chn_id, yt_channels) do
    from(r in Registration,
      where: r.discord_chn_id == ^dc_chn_id,
      join: c in assoc(r, :channel),
      prefix: "clip",
      where: c in ^yt_channels,
      select: r
    )
    |> Repo.delete_all()
  end

  def commands do
    [@register, @unregister]
  end
end
