defmodule Mobilizon.Federation.ActivityStream.Converter.Resource do
  @moduledoc """
  Resource converter.

  This module allows to convert resources from ActivityStream format to our own
  internal one, and back.
  """
  alias Mobilizon.Actors.Actor
  alias Mobilizon.Federation.ActivityPub
  alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
  alias Mobilizon.Federation.ActivityPub.Utils
  alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
  alias Mobilizon.Resources
  alias Mobilizon.Resources.Resource
  require Logger

  @behaviour Converter

  defimpl Convertible, for: Resource do
    alias Mobilizon.Federation.ActivityStream.Converter.Resource, as: ResourceConverter

    defdelegate model_to_as(resource), to: ResourceConverter
  end

  @doc """
  Convert an resource struct to an ActivityStream representation
  """
  @impl Converter
  @spec model_to_as(Resource.t()) :: map
  def model_to_as(
        %Resource{actor: %Actor{url: actor_url}, creator: %Actor{url: creator_url}, type: type} =
          resource
      ) do
    res = %{
      "actor" => creator_url,
      "id" => resource.url,
      "name" => resource.title,
      "summary" => resource.summary,
      "context" => get_context(resource),
      "attributedTo" => actor_url,
      "published" => resource.published_at |> DateTime.to_iso8601()
    }

    case type do
      :folder ->
        Map.put(res, "type", "ResourceCollection")

      _ ->
        res
        |> Map.put("type", "Document")
        |> Map.put("url", resource.resource_url)
    end
  end

  @doc """
  Converts an AP object data to our internal data structure.
  """
  @impl Converter
  @spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
  def as_to_model_data(%{"type" => type, "actor" => creator, "attributedTo" => group} = object) do
    with {:ok, %Actor{id: actor_id, resources_url: resources_url}} <- get_actor(group),
         {:ok, %Actor{id: creator_id}} <- get_actor(creator),
         parent_id <- get_parent_id(object["context"], resources_url) do
      data = %{
        title: object["name"],
        summary: object["summary"],
        url: object["id"],
        actor_id: actor_id,
        creator_id: creator_id,
        parent_id: parent_id,
        published_at: object["published"]
      }

      case type do
        "Document" ->
          data
          |> Map.put(:type, :link)
          |> Map.put(:resource_url, object["url"])

        "ResourceCollection" ->
          data
          |> Map.put(:type, :folder)
      end
    else
      {:error, err} -> {:error, err}
      err -> {:error, err}
    end
  end

  @spec get_actor(String.t() | map() | nil) :: {:ok, Actor.t()} | {:error, String.t()}
  defp get_actor(nil), do: {:error, "nil property found for actor data"}

  defp get_actor(actor),
    do: actor |> Utils.get_url() |> ActivityPubActor.get_or_fetch_actor_by_url()

  defp get_context(%Resource{parent_id: nil, actor: %Actor{resources_url: resources_url}}),
    do: resources_url

  defp get_context(%Resource{parent: %Resource{url: url}}), do: url

  defp get_context(%Resource{parent_id: parent_id}),
    do: parent_id |> Resources.get_resource() |> Map.get(:url)

  @spec get_parent_id(String.t(), String.t()) :: Resource.t() | map()
  defp get_parent_id(context, resources_url) do
    Logger.debug(
      "Getting parentID for context #{inspect(context)} and with resources_url #{
        inspect(resources_url)
      }"
    )

    case Utils.get_url(context) do
      nil -> nil
      ^resources_url -> nil
      context_url -> fetch_resource(context_url)
    end
  end

  defp fetch_resource(context_url) do
    case Resources.get_resource_by_url(context_url) do
      %Resource{id: resource_id} = _resource ->
        resource_id

      nil ->
        case ActivityPub.fetch_object_from_url(context_url) do
          {:ok, %Resource{id: resource_id} = _resource} ->
            resource_id

          _ ->
            nil
        end
    end
  end
end