defmodule Mobilizon.GraphQL.Resolvers.Participant do
  @moduledoc """
  Handles the participation-related GraphQL calls.
  """
  alias Mobilizon.{Actors, Config, Crypto, Events}
  alias Mobilizon.Actors.Actor
  alias Mobilizon.Events.{Event, Participant}
  alias Mobilizon.GraphQL.API.Participations
  alias Mobilizon.Service.Export.Participants.{CSV, ODS, PDF}
  alias Mobilizon.Users.User
  alias Mobilizon.Web.Email
  alias Mobilizon.Web.Email.Checker
  require Logger
  import Mobilizon.Web.Gettext
  import Mobilizon.GraphQL.Resolvers.Event.Utils

  @doc """
  Join an event for an regular or anonymous actor
  """
  @spec actor_join_event(any(), map(), Absinthe.Resolution.t()) ::
          {:ok, Participant.t()} | {:error, String.t()}
  def actor_join_event(
        _parent,
        %{actor_id: actor_id, event_id: event_id} = args,
        %{context: %{current_user: %User{} = user}}
      ) do
    case User.owns_actor(user, actor_id) do
      {:is_owned, %Actor{} = actor} ->
        do_actor_join_event(actor, event_id, args)

      _ ->
        {:error, dgettext("errors", "Profile is not owned by authenticated user")}
    end
  end

  def actor_join_event(
        _parent,
        %{actor_id: actor_id, event_id: event_id} = args,
        _resolution
      ) do
    with {:has_event, {:ok, %Event{} = event}} <-
           {:has_event, Mobilizon.Events.get_event_with_preload(event_id)},
         {:anonymous_participation_enabled, true} <-
           {:anonymous_participation_enabled,
            event.local == true && Config.anonymous_participation?() &&
              event.options.anonymous_participation == true},
         {:anonymous_actor_id, true} <-
           {:anonymous_actor_id, to_string(Config.anonymous_actor_id()) == actor_id},
         {:email_required, true} <-
           {:email_required,
            Config.anonymous_participation_email_required?() &&
              args |> Map.get(:email) |> valid_email?()},
         {:confirmation_token, {confirmation_token, role}} <-
           {:confirmation_token,
            if(Config.anonymous_participation_email_confirmation_required?(),
              do: {Crypto.random_string(30), :not_confirmed},
              else: {nil, :participant}
            )},
         # We only federate if the participation is not to be confirmed later
         args <-
           args
           |> Map.put(:confirmation_token, confirmation_token)
           |> Map.put(:cancellation_token, Crypto.random_string(30))
           |> Map.put(:role, role)
           |> Map.put(:local, role == :participant),
         {:actor_not_found, %Actor{} = actor} <-
           {:actor_not_found, Actors.get_actor_with_preload(actor_id)},
         {:ok, %Participant{} = participant} <- do_actor_join_event(actor, event_id, args) do
      if Config.anonymous_participation_email_required?() &&
           Config.anonymous_participation_email_confirmation_required?() do
        args
        |> Map.get(:email)
        |> Email.Participation.anonymous_participation_confirmation(
          participant,
          Map.get(args, :locale, "en")
        )
        |> Email.Mailer.send_email()
      end

      {:ok, participant}
    else
      {:error, err} ->
        {:error, err}

      {:has_event, _} ->
        {:error,
         dgettext("errors", "Event with this ID %{id} doesn't exist", id: inspect(event_id))}

      {:anonymous_participation_enabled, false} ->
        {:error, dgettext("errors", "Anonymous participation is not enabled")}

      {:anonymous_actor_id, false} ->
        {:error, dgettext("errors", "Profile ID provided is not the anonymous profile one")}

      {:email_required, _} ->
        {:error, dgettext("errors", "A valid email is required by your instance")}

      {:actor_not_found, _} ->
        Logger.error(
          "The actor ID \"#{actor_id}\" provided by configuration doesn't match any actor in database"
        )

        {:error, dgettext("errors", "Internal Error")}
    end
  end

  def actor_join_event(_parent, _args, _resolution) do
    {:error, dgettext("errors", "You need to be logged-in to join an event")}
  end

  @spec do_actor_join_event(Actor.t(), integer | String.t(), map()) ::
          {:ok, Participant.t()} | {:error, String.t()}
  defp do_actor_join_event(actor, event_id, args) do
    with {:has_event, {:ok, %Event{} = event}} <-
           {:has_event, Events.get_event_with_preload(event_id)},
         {:ok, _activity, participant} <- Participations.join(event, actor, args),
         %Participant{} = participant <-
           participant
           |> Map.put(:event, event)
           |> Map.put(:actor, actor) do
      {:ok, participant}
    else
      {:error, :maximum_attendee_capacity_reached} ->
        {:error, dgettext("errors", "The event has already reached its maximum capacity")}

      {:has_event, _} ->
        {:error,
         dgettext("errors", "Event with this ID %{id} doesn't exist", id: inspect(event_id))}

      {:error, :event_not_found} ->
        {:error, dgettext("errors", "Event id not found")}

      {:error, :already_participant} ->
        {:error, dgettext("errors", "You are already a participant of this event")}
    end
  end

  @spec check_anonymous_participation(String.t(), String.t()) ::
          {:ok, Event.t()} | {:error, String.t()}
  defp check_anonymous_participation(actor_id, event_id) do
    cond do
      Config.anonymous_participation?() == false ->
        {:error, dgettext("errors", "Anonymous participation is not enabled")}

      to_string(Config.anonymous_actor_id()) != actor_id ->
        {:error, dgettext("errors", "The anonymous actor ID is invalid")}

      true ->
        case Mobilizon.Events.get_event_with_preload(event_id) do
          {:ok, %Event{} = event} ->
            {:ok, event}

          {:error, :event_not_found} ->
            {:error,
             dgettext("errors", "Event with this ID %{id} doesn't exist", id: inspect(event_id))}
        end
    end
  end

  @doc """
  Leave an event for an anonymous actor
  """
  @spec actor_leave_event(any(), map(), Absinthe.Resolution.t()) ::
          {:ok, map()} | {:error, String.t()}
  def actor_leave_event(
        _parent,
        %{actor_id: actor_id, event_id: event_id, token: token},
        _resolution
      )
      when not is_nil(token) do
    case check_anonymous_participation(actor_id, event_id) do
      {:ok, %Event{} = event} ->
        %Actor{} = actor = Actors.get_actor_with_preload!(actor_id)

        case Participations.leave(event, actor, %{local: false, cancellation_token: token}) do
          {:ok, _activity, %Participant{id: participant_id} = _participant} ->
            {:ok, %{event: %{id: event_id}, actor: %{id: actor_id}, id: participant_id}}

          {:error, :is_only_organizer} ->
            {:error,
             dgettext(
               "errors",
               "You can't leave event because you're the only event creator participant"
             )}

          {:error, :participant_not_found} ->
            {:error, dgettext("errors", "Participant not found")}

          {:error, _err} ->
            {:error, dgettext("errors", "Failed to leave the event")}
        end
    end
  end

  def actor_leave_event(
        _parent,
        %{actor_id: actor_id, event_id: event_id},
        %{context: %{current_user: user}}
      ) do
    with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
         {:has_event, {:ok, %Event{} = event}} <-
           {:has_event, Events.get_event_with_preload(event_id)},
         {:ok, _activity, _participant} <- Participations.leave(event, actor) do
      {:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}}
    else
      {:has_event, _} ->
        {:error, "Event with this ID #{inspect(event_id)} doesn't exist"}

      {:is_owned, nil} ->
        {:error, dgettext("errors", "Profile is not owned by authenticated user")}

      {:error, :is_only_organizer} ->
        {:error,
         dgettext(
           "errors",
           "You can't leave event because you're the only event creator participant"
         )}

      {:error, :participant_not_found} ->
        {:error, dgettext("errors", "Participant not found")}
    end
  end

  def actor_leave_event(_parent, _args, _resolution) do
    {:error, dgettext("errors", "You need to be logged-in to leave an event")}
  end

  @spec update_participation(any(), map(), Absinthe.Resolution.t()) ::
          {:ok, Participation.t()} | {:error, String.t() | Ecto.Changeset.t()}
  def update_participation(
        _parent,
        %{id: participation_id, role: new_role},
        %{
          context: %{
            current_actor: %Actor{} = moderator_actor
          }
        }
      ) do
    # Check that participation already exists

    case Events.get_participant(participation_id) do
      %Participant{role: old_role, event_id: event_id} = participation ->
        if new_role != old_role do
          %Event{} = event = Events.get_event_with_preload!(event_id)

          if can_event_be_updated_by?(event, moderator_actor) do
            with {:ok, _activity, participation} <-
                   Participations.update(participation, moderator_actor, new_role) do
              {:ok, participation}
            end
          else
            {:error,
             dgettext(
               "errors",
               "Provided profile doesn't have moderator permissions on this event"
             )}
          end
        else
          {:error, dgettext("errors", "Participant already has role %{role}", role: new_role)}
        end

      nil ->
        {:error, dgettext("errors", "Participant not found")}
    end
  end

  @spec confirm_participation_from_token(map(), map(), map()) ::
          {:ok, Participant.t()} | {:error, String.t()}
  def confirm_participation_from_token(
        _parent,
        %{confirmation_token: confirmation_token},
        _context
      ) do
    with {:has_participant,
          %Participant{actor: actor, role: :not_confirmed, event: event} = participant} <-
           {:has_participant, Events.get_participant_by_confirmation_token(confirmation_token)},
         {:ok, _activity, %Participant{} = participant} <-
           Participations.update(participant, actor, Events.get_default_participant_role(event)) do
      {:ok, participant}
    else
      {:has_participant, nil} ->
        {:error, dgettext("errors", "This token is invalid")}

      {:error, %Ecto.Changeset{} = err} ->
        {:error, err}
    end
  end

  @spec export_event_participants(any(), map(), Absinthe.Resolution.t()) :: {:ok, String.t()}
  def export_event_participants(_parent, %{event_id: event_id, roles: roles, format: format}, %{
        context: %{
          current_user: %User{locale: locale},
          current_actor: %Actor{} = moderator_actor
        }
      }) do
    case Events.get_event_with_preload(event_id) do
      {:ok, %Event{} = event} ->
        if can_event_be_updated_by?(event, moderator_actor) do
          case export_format(format, event, roles, locale) do
            {:ok, path} ->
              {:ok, path}

            {:error, :export_dependency_not_installed} ->
              {:error,
               dgettext(
                 "errors",
                 "A dependency needed to export to %{format} is not installed",
                 format: format
               )}

            {:error, :failed_to_save_upload} ->
              {:error,
               dgettext(
                 "errors",
                 "An error occured while saving export",
                 format: format
               )}

            {:error, :format_not_supported} ->
              {:error,
               dgettext(
                 "errors",
                 "Format not supported"
               )}
          end
        else
          {:error,
           dgettext(
             "errors",
             "Provided profile doesn't have moderator permissions on this event"
           )}
        end

      {:error, :event_not_found} ->
        {:error,
         dgettext("errors", "Event with this ID %{id} doesn't exist", id: inspect(event_id))}
    end
  end

  def export_event_participants(_, _, _), do: {:error, :unauthorized}

  @spec valid_email?(String.t() | nil) :: boolean
  defp valid_email?(email) when is_nil(email), do: false

  defp valid_email?(email) when is_binary(email) do
    email
    |> String.trim()
    |> Checker.valid?()
  end

  @spec export_format(atom(), Event.t(), list(), String.t()) ::
          {:ok, String.t()}
          | {:error,
             :format_not_supported | :export_dependency_not_installed | :failed_to_save_upload}
  defp export_format(format, event, roles, locale) do
    case format do
      :csv ->
        CSV.export(event, roles: roles, locale: locale)

      :pdf ->
        PDF.export(event, roles: roles, locale: locale)

      :ods ->
        ODS.export(event, roles: roles, locale: locale)

      _ ->
        {:error, :format_not_supported}
    end
  end
end
