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

  This module allows to convert reports from ActivityStream format to our own
  internal one, and back.

  Note: Reports are named Flag in AS.
  """

  alias Mobilizon.Actors.Actor
  alias Mobilizon.Discussions.Comment
  alias Mobilizon.Events.Event
  alias Mobilizon.Reports.Report

  alias Mobilizon.Federation.ActivityPub
  alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
  alias Mobilizon.Federation.ActivityPub.Relay
  alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}

  @behaviour Converter

  defimpl Convertible, for: Report do
    alias Mobilizon.Federation.ActivityStream.Converter.Flag, as: FlagConverter

    defdelegate model_to_as(report), to: FlagConverter
  end

  @doc """
  Converts an AP object data to our internal data structure.
  """
  @impl Converter
  @spec as_to_model_data(map) :: map
  def as_to_model_data(object) do
    with params <- as_to_model(object) do
      %{
        "reporter_id" => params["reporter"].id,
        "uri" => params["uri"],
        "content" => params["content"],
        "reported_id" => params["reported"].id,
        "events" => params["events"],
        "comments" => params["comments"]
      }
    end
  end

  @doc """
  Convert an event struct to an ActivityStream representation
  """
  @impl Converter
  @spec model_to_as(Report.t()) :: map
  def model_to_as(%Report{} = report) do
    object =
      [report.reported.url] ++
        Enum.map(report.comments, fn comment -> comment.url end) ++
        Enum.map(report.events, & &1.url)

    %{
      "type" => "Flag",
      "actor" => Relay.get_actor().url,
      "id" => report.url,
      "content" => report.content,
      "mediaType" => "text/plain",
      "object" => object
    }
  end

  @spec as_to_model(map) :: map
  def as_to_model(%{"object" => objects} = object) do
    with {:ok, %Actor{} = reporter} <-
           ActivityPubActor.get_or_fetch_actor_by_url(object["actor"]),
         %Actor{} = reported <- find_reported(objects),
         %{events: events, comments: comments} <- find_events_and_comments(objects) do
      %{
        "reporter" => reporter,
        "uri" => object["id"],
        "content" => object["content"],
        "reported" => reported,
        "events" => events,
        "comments" => comments
      }
    end
  end

  @spec find_reported(list(String.t())) :: Actor.t() | nil
  defp find_reported(objects) do
    Enum.reduce_while(objects, nil, fn url, _ ->
      case ActivityPubActor.get_or_fetch_actor_by_url(url) do
        {:ok, %Actor{} = actor} ->
          {:halt, actor}

        _ ->
          {:cont, nil}
      end
    end)
  end

  defp find_events_and_comments(objects) do
    objects
    |> Enum.map(&ActivityPub.fetch_object_from_url/1)
    |> Enum.reduce(%{comments: [], events: []}, fn res, acc ->
      case res do
        {:ok, %Event{} = event} ->
          Map.put(acc, :events, [event | acc.events])

        {:ok, %Comment{} = comment} ->
          Map.put(acc, :comments, [comment | acc.comments])

        _ ->
          acc
      end
    end)
  end
end