//! traits around accepting activities

use crate::{
    digest::DigestFactory,
    repo::{Dereference, Repo},
    session::Session,
    signature::{PrivateKey, PrivateKeyBuilder},
};
use std::{rc::Rc, sync::Arc};
use url::{Host, Url};

/// The Authority that is providing the ingested data
///
/// - An Authority of None means the data is untrustworthy, as no entity can be verified to have
///    provided this data
/// - An Authority of Server means the data to be ingested has been provided on behalf of it's
///   origin server. A URL is provided in the Server variant to describe the specific URL the data
///   originates from.
/// - An Authority of Actor(Url) means the data to be ingested has been provided on behalf of the
///   Actor identified by the associated URL
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Authority {
    /// No Authority provided
    None,

    /// The Authority for ingested data is the server the data is hosted on
    Server(Url),

    /// The Authority for the ingested data is the provided url
    Actor(Url),
}

/// Describes accepting a new Object into the system
///
/// This type is implemented by users of `apub` to hook into provied inbox methods
#[async_trait::async_trait(?Send)]
pub trait Ingest<Object> {
    /// The error produced when ingesting activities
    type Error;

    /// The actor that is receiving the activity
    ///
    /// e.g.
    /// - the community that is receiving a new post
    /// - the server actor (in the case of the shared inbox)
    type ActorId;

    /// Accept and process a given activity
    async fn ingest<R: FullRepo, S: Session>(
        &self,
        authority: Authority,
        actor_id: Self::ActorId,
        activity: Object,
        repo: R,
        remote_repo: &R::Remote,
        session: S,
    ) -> Result<(), R::Error>
    where
        R::Error: From<Self::Error>;
}

/// A trait representing a full local + remote + ingest combination repository
#[async_trait::async_trait(?Send)]
pub trait FullRepo {
    /// Errors produced by the Full Repository
    type Error: From<<Self::Local as Repo>::Error> + From<<Self::Remote as Repo>::Error>;

    /// The local object repository
    type Local: Repo;

    /// The cryptography implementation for the remote repo
    type Crypto: DigestFactory + PrivateKey + PrivateKeyBuilder;

    /// The remote object repository
    type Remote: Repo;

    /// The ingester
    type Ingest;

    /// Domain or IP of the local server
    fn local_domain(&self) -> &Host<String>;

    /// Port of the local server
    fn local_port(&self) -> Option<u16>;

    /// get the local repo
    fn local(&self) -> &Self::Local;

    /// get the remote repo
    fn remote(&self, crypto: Self::Crypto) -> Self::Remote;

    /// get the ingester
    fn ingest(&self) -> &Self::Ingest;

    /// Determine if a given URL is local
    fn is_local(&self, url: &Url) -> bool {
        Some(borrow_host(self.local_domain())) == url.host() && self.local_port() == url.port()
    }

    /// Ingest an activity
    async fn ingest_activity<Object, S: Session>(
        &self,
        authority: Authority,
        actor_id: <Self::Ingest as Ingest<Object>>::ActorId,
        activity: Object,
        remote_repo: &Self::Remote,
        session: S,
    ) -> Result<(), Self::Error>
    where
        Self: Sized,
        Self::Ingest: Ingest<Object>,
        Self::Error: From<<Self::Ingest as Ingest<Object>>::Error>,
    {
        self.ingest()
            .ingest(authority, actor_id, activity, self, remote_repo, session)
            .await
    }

    /// Fetch the ID, ingesting it if it isn't local
    async fn fetch<D: Dereference, S: Session>(
        &self,
        id: D,
        mut session: S,
        actor_id: <Self::Ingest as Ingest<D::Output>>::ActorId,
        remote_repo: &Self::Remote,
    ) -> Result<Option<D::Output>, Self::Error>
    where
        Self: Sized,
        D::Output: Clone,
        Self::Ingest: Ingest<D::Output>,
        <Self::Ingest as Ingest<D::Output>>::ActorId: From<Url>,
        Self::Error: From<<Self::Ingest as Ingest<D::Output>>::Error>,
    {
        let url = id.url().clone();

        let opt = self.local().fetch(&id, &mut session).await?;

        if self.is_local(&url) {
            return Ok(opt);
        }

        let opt = remote_repo.fetch(id, &mut session).await?;

        if let Some(value) = opt.clone() {
            self.ingest_activity(
                Authority::Server(url),
                actor_id,
                value,
                remote_repo,
                session,
            )
            .await?;
        }

        Ok(opt)
    }
}

impl std::fmt::Display for Authority {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Authority::Actor(actor_id) => write!(f, "Actor - '{}'", actor_id),
            Authority::Server(url) => write!(f, "Server URL - '{}'", url),
            Authority::None => write!(f, "None"),
        }
    }
}

fn borrow_host(host: &Host<String>) -> Host<&str> {
    match host {
        Host::Ipv4(ip) => Host::Ipv4(*ip),
        Host::Ipv6(ip) => Host::Ipv6(*ip),
        Host::Domain(ref domain) => Host::Domain(domain),
    }
}

#[async_trait::async_trait(?Send)]
impl<'a, Object, T> Ingest<Object> for &'a T
where
    T: Ingest<Object>,
    Object: 'static,
{
    type Error = T::Error;
    type ActorId = T::ActorId;

    async fn ingest<R: FullRepo, S: Session>(
        &self,
        authority: Authority,
        actor_id: Self::ActorId,
        activity: Object,
        repo: R,
        remote_repo: &R::Remote,
        session: S,
    ) -> Result<(), R::Error>
    where
        R::Error: From<Self::Error>,
    {
        T::ingest(
            self,
            authority,
            actor_id,
            activity,
            repo,
            remote_repo,
            session,
        )
        .await
    }
}

#[async_trait::async_trait(?Send)]
impl<'a, Object, T> Ingest<Object> for &'a mut T
where
    T: Ingest<Object>,
    Object: 'static,
{
    type Error = T::Error;
    type ActorId = T::ActorId;

    async fn ingest<R: FullRepo, S: Session>(
        &self,
        authority: Authority,
        actor_id: Self::ActorId,
        activity: Object,
        repo: R,
        remote_repo: &R::Remote,
        session: S,
    ) -> Result<(), R::Error>
    where
        R::Error: From<Self::Error>,
    {
        T::ingest(
            self,
            authority,
            actor_id,
            activity,
            repo,
            remote_repo,
            session,
        )
        .await
    }
}

#[async_trait::async_trait(?Send)]
impl<Object, T> Ingest<Object> for Box<T>
where
    T: Ingest<Object>,
    Object: 'static,
{
    type Error = T::Error;
    type ActorId = T::ActorId;

    async fn ingest<R: FullRepo, S: Session>(
        &self,
        authority: Authority,
        actor_id: Self::ActorId,
        activity: Object,
        repo: R,
        remote_repo: &R::Remote,
        session: S,
    ) -> Result<(), R::Error>
    where
        R::Error: From<Self::Error>,
    {
        T::ingest(
            self,
            authority,
            actor_id,
            activity,
            repo,
            remote_repo,
            session,
        )
        .await
    }
}

#[async_trait::async_trait(?Send)]
impl<Object, T> Ingest<Object> for Rc<T>
where
    T: Ingest<Object>,
    Object: 'static,
{
    type Error = T::Error;
    type ActorId = T::ActorId;

    async fn ingest<R: FullRepo, S: Session>(
        &self,
        authority: Authority,
        actor_id: Self::ActorId,
        activity: Object,
        repo: R,
        remote_repo: &R::Remote,
        session: S,
    ) -> Result<(), R::Error>
    where
        R::Error: From<Self::Error>,
    {
        T::ingest(
            self,
            authority,
            actor_id,
            activity,
            repo,
            remote_repo,
            session,
        )
        .await
    }
}

#[async_trait::async_trait(?Send)]
impl<Object, T> Ingest<Object> for Arc<T>
where
    T: Ingest<Object>,
    Object: 'static,
{
    type Error = T::Error;
    type ActorId = T::ActorId;

    async fn ingest<R: FullRepo, S: Session>(
        &self,
        authority: Authority,
        actor_id: Self::ActorId,
        activity: Object,
        repo: R,
        remote_repo: &R::Remote,
        session: S,
    ) -> Result<(), R::Error>
    where
        R::Error: From<Self::Error>,
    {
        T::ingest(
            self,
            authority,
            actor_id,
            activity,
            repo,
            remote_repo,
            session,
        )
        .await
    }
}

#[async_trait::async_trait(?Send)]
impl<'a, T> FullRepo for &'a T
where
    T: FullRepo,
{
    type Error = T::Error;
    type Local = T::Local;
    type Crypto = T::Crypto;
    type Remote = T::Remote;
    type Ingest = T::Ingest;

    fn local_domain(&self) -> &Host<String> {
        T::local_domain(self)
    }

    fn local_port(&self) -> Option<u16> {
        T::local_port(self)
    }

    fn local(&self) -> &Self::Local {
        T::local(self)
    }

    fn remote(&self, crypto: Self::Crypto) -> Self::Remote {
        T::remote(self, crypto)
    }

    fn ingest(&self) -> &Self::Ingest {
        T::ingest(self)
    }
}

#[async_trait::async_trait(?Send)]
impl<'a, T> FullRepo for &'a mut T
where
    T: FullRepo,
{
    type Error = T::Error;
    type Local = T::Local;
    type Crypto = T::Crypto;
    type Remote = T::Remote;
    type Ingest = T::Ingest;

    fn local_domain(&self) -> &Host<String> {
        T::local_domain(self)
    }

    fn local_port(&self) -> Option<u16> {
        T::local_port(self)
    }

    fn local(&self) -> &Self::Local {
        T::local(self)
    }

    fn remote(&self, crypto: Self::Crypto) -> Self::Remote {
        T::remote(self, crypto)
    }

    fn ingest(&self) -> &Self::Ingest {
        T::ingest(self)
    }
}

#[async_trait::async_trait(?Send)]
impl<T> FullRepo for Box<T>
where
    T: FullRepo,
{
    type Error = T::Error;
    type Local = T::Local;
    type Crypto = T::Crypto;
    type Remote = T::Remote;
    type Ingest = T::Ingest;

    fn local_domain(&self) -> &Host<String> {
        T::local_domain(self)
    }

    fn local_port(&self) -> Option<u16> {
        T::local_port(self)
    }

    fn local(&self) -> &Self::Local {
        T::local(self)
    }

    fn remote(&self, crypto: Self::Crypto) -> Self::Remote {
        T::remote(self, crypto)
    }

    fn ingest(&self) -> &Self::Ingest {
        T::ingest(self)
    }
}

#[async_trait::async_trait(?Send)]
impl<T> FullRepo for Rc<T>
where
    T: FullRepo,
{
    type Error = T::Error;
    type Local = T::Local;
    type Crypto = T::Crypto;
    type Remote = T::Remote;
    type Ingest = T::Ingest;

    fn local_domain(&self) -> &Host<String> {
        T::local_domain(self)
    }

    fn local_port(&self) -> Option<u16> {
        T::local_port(self)
    }

    fn local(&self) -> &Self::Local {
        T::local(self)
    }

    fn remote(&self, crypto: Self::Crypto) -> Self::Remote {
        T::remote(self, crypto)
    }

    fn ingest(&self) -> &Self::Ingest {
        T::ingest(self)
    }
}

#[async_trait::async_trait(?Send)]
impl<T> FullRepo for Arc<T>
where
    T: FullRepo,
{
    type Error = T::Error;
    type Local = T::Local;
    type Crypto = T::Crypto;
    type Remote = T::Remote;
    type Ingest = T::Ingest;

    fn local_domain(&self) -> &Host<String> {
        T::local_domain(self)
    }

    fn local_port(&self) -> Option<u16> {
        T::local_port(self)
    }

    fn local(&self) -> &Self::Local {
        T::local(self)
    }

    fn remote(&self, crypto: Self::Crypto) -> Self::Remote {
        T::remote(self, crypto)
    }

    fn ingest(&self) -> &Self::Ingest {
        T::ingest(self)
    }
}
