//! Extensions to the ActivityPub traits

use crate::{
    activitypub::{Activity, Actor},
    ingest::{FullRepo, Ingest},
    repo::Dereference,
    session::Session,
};
use std::{rc::Rc, sync::Arc};
use url::Url;

/// Simplification for <Blah as Dereference>::Output
pub type Out<D> = <D as Dereference>::Output;

/// An extension of Activity that is aware of it's Actor and Object types
#[async_trait::async_trait(?Send)]
pub trait ActivityExt: Activity {
    /// The Actor ID type for dereferencing an Actor
    type ActorId: Dereference + From<Url>;

    /// The Object ID type for dereferencing an Object
    type ObjectId: Dereference + From<Url>;

    /// Attempt to pull the actor off the activity
    fn actor(&self) -> Option<Out<Self::ActorId>>;

    /// Attempt to pull the object off the activity
    fn object(&self) -> Option<Out<Self::ObjectId>>;

    /// Pull the actor off the activity, or fetch it from a repo if it isn't present
    async fn dereference_actor<R: FullRepo, S: Session>(
        &self,
        actor_id: <R::Ingest as Ingest<Out<Self::ActorId>>>::ActorId,
        repo: R,
        remote: &R::Remote,
        session: S,
    ) -> Result<Option<Out<Self::ActorId>>, R::Error>
    where
        R::Ingest: Ingest<Out<Self::ActorId>>,
        <R::Ingest as Ingest<Out<Self::ActorId>>>::ActorId: From<Url>,
        R::Error: From<<R::Ingest as Ingest<Out<Self::ActorId>>>::Error>,
        Out<Self::ActorId>: Clone,
    {
        dereference::<Self::ActorId, _, _>(
            actor_id,
            repo,
            remote,
            session,
            self.actor(),
            self.actor_id(),
        )
        .await
    }

    /// Pull the object off the activity, or fetch it from a repo if it isn't present
    async fn dereference_object<R: FullRepo, S: Session>(
        &self,
        actor_id: <R::Ingest as Ingest<<Self::ObjectId as Dereference>::Output>>::ActorId,
        repo: R,
        remote: &R::Remote,
        session: S,
    ) -> Result<Option<Out<Self::ObjectId>>, R::Error>
    where
        R::Ingest: Ingest<Out<Self::ObjectId>>,
        <R::Ingest as Ingest<Out<Self::ObjectId>>>::ActorId: From<Url>,
        R::Error: From<<R::Ingest as Ingest<Out<Self::ObjectId>>>::Error>,
        Out<Self::ObjectId>: Clone,
    {
        dereference::<Self::ObjectId, _, _>(
            actor_id,
            repo,
            remote,
            session,
            self.object(),
            self.object_id(),
        )
        .await
    }
}

/// An extension of Actor that is aware of the PublicKey type
#[async_trait::async_trait(?Send)]
pub trait ActorExt: Actor {
    /// The Public Key ID for dereferencing a Public key
    type PublicKeyId: Dereference + From<Url>;

    /// Attempt to pull the public key off the actor
    fn public_key(&self) -> Option<Out<Self::PublicKeyId>>;

    /// Pull the public key off the acctor, or fetch it from a repo if it isn't present
    async fn dereference_public_key<R: FullRepo, S: Session>(
        &self,
        actor_id: <R::Ingest as Ingest<Out<Self::PublicKeyId>>>::ActorId,
        repo: R,
        remote: &R::Remote,
        session: S,
    ) -> Result<Option<Out<Self::PublicKeyId>>, R::Error>
    where
        R::Ingest: Ingest<Out<Self::PublicKeyId>>,
        <R::Ingest as Ingest<Out<Self::PublicKeyId>>>::ActorId: From<Url>,
        R::Error: From<<R::Ingest as Ingest<Out<Self::PublicKeyId>>>::Error>,
        Out<Self::PublicKeyId>: Clone,
    {
        dereference::<Self::PublicKeyId, _, _>(
            actor_id,
            repo,
            remote,
            session,
            self.public_key(),
            self.public_key_id(),
        )
        .await
    }
}

/// implementation to dereference an object through FullRepo if needed
pub async fn dereference<D: Dereference + From<Url>, R: FullRepo, S: Session>(
    actor_id: <R::Ingest as Ingest<Out<D>>>::ActorId,
    repo: R,
    remote: &R::Remote,
    session: S,
    output: Option<D::Output>,
    id: &Url,
) -> Result<Option<D::Output>, R::Error>
where
    R::Ingest: Ingest<Out<D>>,
    <R::Ingest as Ingest<Out<D>>>::ActorId: From<Url>,
    R::Error: From<<R::Ingest as Ingest<Out<D>>>::Error>,
    D::Output: Clone,
{
    if let Some(item) = output {
        return Ok(Some(item));
    }

    repo.fetch(D::from(id.clone()), session, actor_id, remote)
        .await
}

impl<'a, T> ActivityExt for &'a T
where
    T: ActivityExt,
{
    type ActorId = T::ActorId;
    type ObjectId = T::ObjectId;

    fn actor(&self) -> Option<<Self::ActorId as Dereference>::Output> {
        T::actor(self)
    }

    fn object(&self) -> Option<<Self::ObjectId as Dereference>::Output> {
        T::object(self)
    }
}

impl<'a, T> ActivityExt for &'a mut T
where
    T: ActivityExt,
{
    type ActorId = T::ActorId;
    type ObjectId = T::ObjectId;

    fn actor(&self) -> Option<<Self::ActorId as Dereference>::Output> {
        T::actor(self)
    }

    fn object(&self) -> Option<<Self::ObjectId as Dereference>::Output> {
        T::object(self)
    }
}

impl<T> ActivityExt for Box<T>
where
    T: ActivityExt,
{
    type ActorId = T::ActorId;
    type ObjectId = T::ObjectId;

    fn actor(&self) -> Option<<Self::ActorId as Dereference>::Output> {
        T::actor(self)
    }

    fn object(&self) -> Option<<Self::ObjectId as Dereference>::Output> {
        T::object(self)
    }
}

impl<T> ActivityExt for Rc<T>
where
    T: ActivityExt,
{
    type ActorId = T::ActorId;
    type ObjectId = T::ObjectId;

    fn actor(&self) -> Option<<Self::ActorId as Dereference>::Output> {
        T::actor(self)
    }

    fn object(&self) -> Option<<Self::ObjectId as Dereference>::Output> {
        T::object(self)
    }
}

impl<T> ActivityExt for Arc<T>
where
    T: ActivityExt,
{
    type ActorId = T::ActorId;
    type ObjectId = T::ObjectId;

    fn actor(&self) -> Option<<Self::ActorId as Dereference>::Output> {
        T::actor(self)
    }

    fn object(&self) -> Option<<Self::ObjectId as Dereference>::Output> {
        T::object(self)
    }
}
