//! Shared loader-blob upload primitive.
//!
//! Sway contract bytecode bigger than one blob-upload transaction
//! can't be deployed by uploading a single blob and pointing a proxy
//! at it: the proxy's `run_external_blob` LDCs exactly one blob. The
//! workaround the SDK gives us is `convert_to_loader(max_words)`,
//! which splits the bytecode into `data_blobs` and generates a small
//! loader stub that LDCs each data blob in order before jumping into
//! the reconstituted code. We upload the data blobs first, then
//! upload the loader stub *as its own blob*, and the proxy points at
//! the loader blob.
//!
//! Both `OrderBookDeploy` / `TradeAccountDeploy` and the contract
//! integration tests go through this module so the upload flow stays
//! in one place. Adding it here, instead of inlining it in each
//! deployer, avoids the silent-bug class where one site forgets to
//! upload the data blobs (the loader blob would still upload fine,
//! the proxy would point at it, and the first call would fail with
//! `TransactionSizeLimitExceeded` or a missing-blob LDC trap).

use anyhow::Result;
use fuels::{
    core::Configurables,
    prelude::*,
    tx::StorageSlot,
    types::transaction_builders::Blob,
};

/// Build the `(data_blobs, loader_blob)` pair for `bytecode`. The
/// loader blob's id is the value to use as the proxy's
/// `INITIAL_TARGET`; `data_blobs` are the chunked code pieces it
/// references and must be uploaded first.
pub fn build_loader_blobs(
    bytecode: Vec<u8>,
    salt: Salt,
    storage_slots: Vec<StorageSlot>,
    configurables: impl Into<Configurables>,
    max_words_per_blob: usize,
) -> Result<(Vec<Blob>, Blob)> {
    let loader = Contract::regular(bytecode, salt, storage_slots)
        .with_configurables(configurables)
        .convert_to_loader(max_words_per_blob)?;
    let data_blobs = loader.blobs().to_vec();
    let loader_blob = Blob::new(loader.code());
    Ok((data_blobs, loader_blob))
}

/// Upload `data_blobs` then `loader_blob`. Blobs already on chain are
/// skipped (`blob_exists` check). Returns the loader blob's id.
pub async fn upload_loader_blobs<W>(
    deployer_wallet: &W,
    data_blobs: Vec<Blob>,
    loader_blob: Blob,
) -> Result<BlobId>
where
    W: Account,
{
    let provider = deployer_wallet.try_provider()?;
    for data_blob in data_blobs {
        if provider.blob_exists(data_blob.id()).await? {
            continue;
        }
        upload_single_blob(deployer_wallet, data_blob).await?;
    }
    let loader_blob_id = loader_blob.id();
    if !provider.blob_exists(loader_blob_id).await? {
        upload_single_blob(deployer_wallet, loader_blob).await?;
    }
    Ok(loader_blob_id)
}

/// Build, fund, sign and submit a single `BlobTransactionBuilder`
/// for `blob`, waiting until it commits.
async fn upload_single_blob<W>(deployer_wallet: &W, blob: Blob) -> Result<()>
where
    W: Account,
{
    let mut builder = BlobTransactionBuilder::default().with_blob(blob);
    deployer_wallet.adjust_for_fee(&mut builder, 0).await?;
    deployer_wallet.add_witnesses(&mut builder)?;
    let tx = builder.build(&deployer_wallet.try_provider()?).await?;
    deployer_wallet
        .try_provider()?
        .send_transaction_and_await_commit(tx)
        .await?
        .check(None)?;
    Ok(())
}
