// Copyright © 2022 The Radicle Link Contributors
use std::iter;

use nonempty::NonEmpty;
use oid::Oid;

use crate::{
    CollaborativeObject, Embed, Evaluate, ObjectId, TypeName, change, change_graph::ChangeGraph,
    history::EntryId,
};

use super::error;

/// Result of an `update` operation.
#[derive(Debug)]
pub struct Updated<T> {
    /// The new head commit of the DAG.
    pub head: Oid,
    /// The newly updated collaborative object.
    pub object: CollaborativeObject<T>,
    /// Entry parents.
    pub parents: Vec<EntryId>,
}

/// The data required to update an object
pub struct Update {
    /// The CRDT changes to add to the object.
    pub changes: NonEmpty<Vec<u8>>,
    /// The object ID of the object to be updated.
    pub object_id: ObjectId,
    /// The typename of the object to be updated.
    pub type_name: TypeName,
    /// The message to add when updating this object.
    pub message: String,
    /// Embedded files.
    pub embeds: Vec<Embed<Oid>>,
}

/// Update an existing [`CollaborativeObject`].
///
/// The `storage` is the backing storage for storing
/// [`crate::Entry`]s at content-addressable locations. Please see
/// [`crate::Store`] for further information.
///
/// The `signer` is expected to be a cryptographic signing key. This
/// ensures that the objects origin is cryptographically verifiable.
///
/// The `resource` is the resource this change lives under, eg. a project.
///
/// The `parents` are other the parents of this object, for example a
/// code commit.
///
/// The `identifier` is a unique id that is passed through to the
/// [`crate::object::Storage`].
///
/// The `args` are the metadata for this [`CollaborativeObject`]
/// update. See [`Update`] for further information.
pub fn update<T, Storage>(
    storage: &Storage,
    signer: &impl crypto::Signer,
    resource: Option<Oid>,
    related: Vec<Oid>,
    identifier: &Storage::Namespace,
    args: Update,
) -> Result<Updated<T>, error::Update>
where
    T: Evaluate<Storage>,
    Storage: crate::object::Storage,
    Storage: change::Storage<
            ObjectId = Oid,
            Parent = Oid,
            PublicKey = crypto::PublicKey,
            Signature = crypto::Signature,
        >,
{
    let Update {
        type_name: ref typename,
        object_id,
        embeds,
        changes,
        message,
    } = args;

    let existing_refs = storage
        .objects(typename, &object_id)
        .map_err(|err| error::Update::Refs { err: Box::new(err) })?;

    let graph = ChangeGraph::load(storage, existing_refs.iter(), typename, &object_id)
        .ok_or(error::Update::NoSuchObject)?;
    let mut object: CollaborativeObject<T> =
        graph.evaluate(storage).map_err(error::Update::evaluate)?;

    // Create a commit for this change, but don't update any references yet.
    let entry = storage
        .store(
            resource,
            related,
            signer,
            change::Template {
                tips: object.history.tips().into_iter().collect(),
                embeds,
                contents: changes,
                type_name: typename.clone(),
                message,
            },
        )
        .map_err(Into::<error::Update>::into)?;
    let head = entry.id;
    let parents = entry.parents.to_vec();

    // Try to apply this change to our object. This prevents storing invalid updates.
    // Note that if this returns with an error, we are left with an unreachable
    // commit object created above. This is fine, as it will eventually get
    // garbage-collected by Git.
    object
        .object
        .apply(&entry, iter::empty(), storage)
        .map_err(error::Update::evaluate)?;
    object.history.extend(entry);

    // Here we actually update the references to point to the new update.
    storage
        .update(identifier, typename, &object_id, &head)
        .map_err(|err| error::Update::Refs { err: Box::new(err) })?;

    Ok(Updated {
        object,
        head,
        parents,
    })
}
