use crate::{
  apub::{
    extensions::signatures::verify,
    fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
    insert_activity,
    ActorType,
  },
  blocking,
  routes::{ChatServerParam, DbPoolParam},
  LemmyError,
};
use activitystreams_new::{
  activity::{Follow, Undo},
  prelude::*,
};
use actix_web::{client::Client, web, HttpRequest, HttpResponse};
use lemmy_db::{
  community::{Community, CommunityFollower, CommunityFollowerForm},
  user::User_,
  Followable,
};
use log::debug;
use serde::Deserialize;
use std::fmt::Debug;

#[serde(untagged)]
#[derive(Deserialize, Debug)]
pub enum CommunityAcceptedObjects {
  Follow(Follow),
  Undo(Undo),
}

impl CommunityAcceptedObjects {
  fn follow(&self) -> Result<Follow, LemmyError> {
    match self {
      CommunityAcceptedObjects::Follow(f) => Ok(f.to_owned()),
      CommunityAcceptedObjects::Undo(u) => {
        Ok(Follow::from_any_base(u.object().as_one().unwrap().to_owned())?.unwrap())
      }
    }
  }
}

/// Handler for all incoming activities to community inboxes.
pub async fn community_inbox(
  request: HttpRequest,
  input: web::Json<CommunityAcceptedObjects>,
  path: web::Path<String>,
  db: DbPoolParam,
  client: web::Data<Client>,
  _chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
  let input = input.into_inner();

  let path = path.into_inner();
  let community = blocking(&db, move |conn| Community::read_from_name(&conn, &path)).await??;

  if !community.local {
    return Err(
      format_err!(
        "Received activity is addressed to remote community {}",
        &community.actor_id
      )
      .into(),
    );
  }
  debug!(
    "Community {} received activity {:?}",
    &community.name, &input
  );
  let follow = input.follow()?;
  let user_uri = follow.actor()?.as_single_xsd_any_uri().unwrap();
  let community_uri = follow.object().as_single_xsd_any_uri().unwrap();

  let user = get_or_fetch_and_upsert_remote_user(&user_uri, &client, &db).await?;
  let community = get_or_fetch_and_upsert_remote_community(community_uri, &client, &db).await?;

  verify(&request, &user)?;

  match input {
    CommunityAcceptedObjects::Follow(f) => handle_follow(f, user, community, &client, db).await,
    CommunityAcceptedObjects::Undo(u) => handle_undo_follow(u, user, community, db).await,
  }
}

/// Handle a follow request from a remote user, adding it to the local database and returning an
/// Accept activity.
async fn handle_follow(
  follow: Follow,
  user: User_,
  community: Community,
  client: &Client,
  db: DbPoolParam,
) -> Result<HttpResponse, LemmyError> {
  insert_activity(user.id, follow.clone(), false, &db).await?;

  let community_follower_form = CommunityFollowerForm {
    community_id: community.id,
    user_id: user.id,
  };

  // This will fail if they're already a follower, but ignore the error.
  blocking(&db, move |conn| {
    CommunityFollower::follow(&conn, &community_follower_form).ok()
  })
  .await?;

  community.send_accept_follow(follow, &client, &db).await?;

  Ok(HttpResponse::Ok().finish())
}

async fn handle_undo_follow(
  undo: Undo,
  user: User_,
  community: Community,
  db: DbPoolParam,
) -> Result<HttpResponse, LemmyError> {
  insert_activity(user.id, undo, false, &db).await?;

  let community_follower_form = CommunityFollowerForm {
    community_id: community.id,
    user_id: user.id,
  };

  // This will fail if they aren't a follower, but ignore the error.
  blocking(&db, move |conn| {
    CommunityFollower::unfollow(&conn, &community_follower_form).ok()
  })
  .await?;

  Ok(HttpResponse::Ok().finish())
}
