defmodule Mobilizon.Federation.ActivityPub.Fetcher do
  @moduledoc """
  Module to handle direct URL ActivityPub fetches to remote content

  If you need to first get cached data, see `Mobilizon.Federation.ActivityPub.fetch_object_from_url/2`
  """
  require Logger

  alias Mobilizon.Federation.HTTPSignatures.Signature
  alias Mobilizon.Federation.ActivityPub.{Relay, Transmogrifier}
  alias Mobilizon.Service.HTTP.ActivityPub, as: ActivityPubClient

  import Mobilizon.Federation.ActivityPub.Utils,
    only: [maybe_date_fetch: 2, sign_fetch: 4, origin_check?: 2]

  @spec fetch(String.t(), Keyword.t()) :: {:ok, map()}
  def fetch(url, options \\ []) do
    on_behalf_of = Keyword.get(options, :on_behalf_of, Relay.get_actor())

    with date <- Signature.generate_date_header(),
         headers <-
           [{:Accept, "application/activity+json"}]
           |> maybe_date_fetch(date)
           |> sign_fetch(on_behalf_of, url, date),
         client <-
           ActivityPubClient.client(headers: headers),
         {:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 <-
           ActivityPubClient.get(client, url) do
      {:ok, data}
    else
      {:ok, %Tesla.Env{status: 410}} ->
        Logger.warn("Resource at #{url} is 410 Gone")
        {:error, "Gone"}

      {:ok, %Tesla.Env{status: 404}} ->
        Logger.warn("Resource at #{url} is 404 Gone")
        {:error, "Not found"}

      {:ok, %Tesla.Env{} = res} ->
        {:error, res}
    end
  end

  @spec fetch_and_create(String.t(), Keyword.t()) :: {:ok, map(), struct()}
  def fetch_and_create(url, options \\ []) do
    with {:ok, data} when is_map(data) <- fetch(url, options),
         {:origin_check, true} <- {:origin_check, origin_check?(url, data)},
         params <- %{
           "type" => "Create",
           "to" => data["to"],
           "cc" => data["cc"],
           "actor" => data["actor"] || data["attributedTo"],
           "attributedTo" => data["attributedTo"] || data["actor"],
           "object" => data
         } do
      Transmogrifier.handle_incoming(params)
    else
      {:origin_check, false} ->
        Logger.warn("Object origin check failed")
        {:error, "Object origin check failed"}

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

  @spec fetch_and_update(String.t(), Keyword.t()) :: {:ok, map(), struct()}
  def fetch_and_update(url, options \\ []) do
    with {:ok, data} when is_map(data) <- fetch(url, options),
         {:origin_check, true} <- {:origin_check, origin_check?(url, data)},
         params <- %{
           "type" => "Update",
           "to" => data["to"],
           "cc" => data["cc"],
           "actor" => data["actor"] || data["attributedTo"],
           "attributedTo" => data["attributedTo"] || data["actor"],
           "object" => data
         } do
      Transmogrifier.handle_incoming(params)
    else
      {:origin_check, false} ->
        Logger.warn("Object origin check failed")
        {:error, "Object origin check failed"}

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