use anyhow::Result;
use chrono::Utc;
use libipld_core::cid::Cid;
use rand_chacha::ChaCha12Rng;
use rand_core::SeedableRng;
use std::{collections::BTreeSet, sync::Arc};
use wnfs::{
    common::{BlockStore, MemoryBlockStore},
    nameaccumulator::NameAccumulator,
    private::{
        forest::{
            hamt::HamtForest,
            proofs::{ForestProofs, ProvingHamtForest},
            traits::PrivateForest,
        },
        AccessKey, PrivateDirectory, PrivateNode,
    },
};
use wnfs_common::Storable;

#[async_std::main]
async fn main() -> Result<()> {
    // In between operations, Alice, Bob, and the persistence service would
    // exchange blocks via bitswap, car mirror or some other protocol.
    // Here we're simplifying by sharing a 'global' block store.
    let store = &MemoryBlockStore::new();

    // Alice creates a private file system with some data.
    // She shares read access with bob by securely transferring the access_key.
    // She also publicly announces bob has access to a certain directory at allowed_write_name.
    let (old_forest_cid, access_key, allowed_write_name) = alice_actions(store).await?;

    // Bob can take the access_key and forest and create writes.
    // The output will be a new state of the forest as well as a set of proofs, proving
    // he didn't touch anything in the file system except what he was allowed to.
    let (proofs, new_forest_cid) = bob_actions(old_forest_cid, access_key, store).await?;

    // A persistence service can check Bob's changes between the forests via his proofs.
    // The service does *not* need read access (it doesn't get to know the access_key)
    // and it only gains limited information from the proofs from Bob.
    // The idea is that in practice the persistence service can accept updates from anyone
    // that were indirectly given access by Alice out-of-bounds, and it will store the updated
    // file system.
    persistence_service_actions(
        old_forest_cid,
        new_forest_cid,
        proofs,
        allowed_write_name,
        store,
    )
    .await
}

/// Alice creates a directory and gives access to it out to someone else.
/// The returned AccessKey gives read access and the NameAccumulator is
/// supposed to be publicly signed for verifyable write access.
async fn alice_actions(store: &impl BlockStore) -> Result<(Cid, AccessKey, NameAccumulator)> {
    let rng = &mut ChaCha12Rng::from_entropy();
    let forest = &mut HamtForest::new_rsa_2048_rc(rng);
    let root_dir = &mut PrivateDirectory::new_rc(&forest.empty_name(), Utc::now(), rng);

    let access_key = root_dir.as_node().store(forest, store, rng).await?;
    let cid = forest.store(store).await?;
    let allowed_name = forest.get_accumulated_name(root_dir.header.get_name());

    Ok((cid, access_key, allowed_name))
}

/// Bob can take the forest, read data using the private ref
/// and prove writes.
async fn bob_actions(
    old_forest_cid: Cid,
    root_dir_access: AccessKey,
    store: &impl BlockStore,
) -> Result<(ForestProofs, Cid)> {
    let hamt_forest = HamtForest::load(&old_forest_cid, store).await?;
    let mut forest = ProvingHamtForest::new(Arc::new(hamt_forest));
    let rng = &mut ChaCha12Rng::from_entropy();

    let mut root_node = PrivateNode::load(&root_dir_access, &forest, store, None).await?;
    let root_dir = root_node.as_dir_mut()?;

    // Do arbitrary writes in any paths you have access to
    root_dir
        .write(
            &["Some".into(), "file.txt".into()],
            true,
            Utc::now(),
            b"Hello, Alice!".to_vec(),
            &mut forest,
            store,
            rng,
        )
        .await?;

    root_dir.as_node().store(&mut forest, store, rng).await?;

    let ProvingHamtForest { forest, proofs } = forest;

    let new_forest_cid = forest.store(store).await?;

    Ok((proofs, new_forest_cid))
}

/// A persistence service can verify write proofs relative to a signed
/// accumulator without read access.
async fn persistence_service_actions(
    old_forest_cid: Cid,
    new_forest_cid: Cid,
    proofs: ForestProofs,
    allowed_access: NameAccumulator,
    store: &impl BlockStore,
) -> Result<()> {
    let old_forest = HamtForest::load(&old_forest_cid, store).await?;
    let new_forest = HamtForest::load(&new_forest_cid, store).await?;

    let forest = ProvingHamtForest::from_proofs(proofs, Arc::new(new_forest));

    forest
        .verify_against_previous_state(&old_forest, &BTreeSet::from([allowed_access]), store)
        .await
}
