// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! Program deployment functionality for the Rialo blockchain.
//!
//! This module provides the ability to deploy programs to the Rialo blockchain using the
//! Loader V4 system, following the patterns established in `crates/rialo/src/sdk/lib.rs`.
//!
//! **Note**: Program deployment functionality requires the `bincode` feature to be enabled.
//! Without this feature, deployment operations will return an error.

#[cfg(all(feature = "bincode", not(target_arch = "wasm32")))]
use std::time::Duration;
#[cfg(feature = "bincode")]
use std::time::{SystemTime, UNIX_EPOCH};

#[cfg(feature = "bincode")]
use futures::future;
#[cfg(all(feature = "bincode", not(target_arch = "wasm32")))]
use rand::Rng;
#[cfg(feature = "bincode")]
use rialo_rex_wasm_v1_loader_interface::instruction as rex_wasm_v1_instruction;
#[cfg(feature = "bincode")]
use rialo_risc_v_loader_interface::instruction::RiscVLoaderInstruction;
#[cfg(feature = "bincode")]
use rialo_s_loader_v4_interface::instruction as loader_v4_instruction;
#[cfg(feature = "bincode")]
use rialo_s_sdk::{signature::Keypair, signer::Signer};
#[cfg(all(feature = "bincode", not(target_arch = "wasm32")))]
use tokio::time::sleep;

#[cfg(feature = "bincode")]
use crate::utils::convert_keyring_to_solana_keypair;
#[cfg(feature = "bincode")]
use crate::{
    error::{Result, RialoError},
    keyring::Keyring,
    program::{
        constants::deployment::{
            BUFFER_BALANCE_FACTOR, DEFAULT_CHUNK_SIZE, DEFAULT_CONFIRMATION_BATCH_SIZE,
            DEFAULT_MAX_RETRIES, DEFAULT_RETRY_BASE_DELAY_MS, DEFAULT_RETRY_MAX_DELAY_MS,
        },
        source::ProgramDataSource,
    },
    rpc::{types::Pubkey, RpcClient},
};

/// Loader type to use for program deployment.
#[cfg(feature = "bincode")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LoaderType {
    /// EBPF programs using Loader V4
    #[default]
    LoaderV4,
    /// RISC-V programs using RISC-V Loader
    RiscV,
    /// WASM programs for REX using RexWasmV1 Loader
    RexWasmV1,
}

/// Program format for deployment, determines loader and bytecode type.
#[cfg(feature = "bincode")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProgramFormat {
    /// PolkaVM bytecode for on-chain execution (default)
    ChainPolkaVm,
    /// WASM component for REX execution
    #[cfg(feature = "wasm-rex")]
    RexWasm,
}

#[cfg(feature = "bincode")]
impl std::str::FromStr for ProgramFormat {
    type Err = RialoError;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "chain/polkavm" => Ok(ProgramFormat::ChainPolkaVm),
            #[cfg(feature = "wasm-rex")]
            "rex/wasm" => Ok(ProgramFormat::RexWasm),
            _ => Err(RialoError::InvalidInput(format!(
                "Invalid program format: '{}'. Valid formats: 'chain/polkavm', 'rex/wasm'",
                s
            ))),
        }
    }
}

#[cfg(feature = "bincode")]
impl ProgramFormat {
    /// Returns the magic bytes for this program format.
    pub fn magic_bytes(&self) -> &'static [u8] {
        match self {
            ProgramFormat::ChainPolkaVm => b"PVM\0",
            #[cfg(feature = "wasm-rex")]
            ProgramFormat::RexWasm => b"\0asm",
        }
    }

    /// Returns the loader type for this program format.
    pub fn loader_type(&self) -> LoaderType {
        match self {
            ProgramFormat::ChainPolkaVm => LoaderType::RiscV,
            #[cfg(feature = "wasm-rex")]
            ProgramFormat::RexWasm => LoaderType::RexWasmV1,
        }
    }

    /// Validates that the program data matches this format's magic bytes.
    pub fn validate_magic_bytes(&self, data: &[u8]) -> Result<()> {
        let magic = self.magic_bytes();
        if data.len() < magic.len() {
            return Err(RialoError::InvalidInput(format!(
                "Program data too small ({} bytes), expected at least {} bytes for {:?} format",
                data.len(),
                magic.len(),
                self
            )));
        }
        if &data[0..magic.len()] != magic {
            return Err(RialoError::InvalidInput(format!(
                "Program magic bytes mismatch. Expected {:?} format with magic {:?}, found {:?}",
                self,
                magic,
                &data[0..magic.len().min(4)]
            )));
        }
        Ok(())
    }
}

/// Configuration options for program deployment.
#[cfg(feature = "bincode")]
#[derive(Debug, Clone)]
pub struct DeploymentConfig {
    /// Size of each chunk when writing program data (in bytes).
    /// Default: [`DEFAULT_CHUNK_SIZE`] (safe for most Solana transaction size limits)
    pub chunk_size: usize,

    /// Maximum number of retries when waiting for buffer account to be ready.
    /// Default: [`DEFAULT_MAX_RETRIES`] retries
    pub max_retries: u32,

    /// Base delay for exponential backoff (ms)
    /// Default: [`DEFAULT_RETRY_BASE_DELAY_MS`]
    pub retry_base_delay_ms: u64,

    /// Maximum retry delay (ms)
    /// Default: [`DEFAULT_RETRY_MAX_DELAY_MS`]
    pub retry_max_delay_ms: u64,

    /// Number of chunks to write before waiting for confirmation.
    /// Default: [`DEFAULT_CONFIRMATION_BATCH_SIZE`]
    pub confirmation_batch_size: usize,
}

/// Calculates exponential backoff delay with optional jitter.
///
/// Uses a 1.15x multiplier for slower ramp-up:
///
/// # Arguments
/// * `attempt` - Current retry attempt (0-indexed)
/// * `base_ms` - Base delay in milliseconds
/// * `max_ms` - Maximum delay in milliseconds
/// * `do_jitter` - Whether to add random jitter (up to 30%)
///
/// # Returns
/// Calculated delay, capped at max_ms
#[cfg(feature = "bincode")]
fn calculate_backoff(attempt: u32, base_ms: u64, max_ms: u64, do_jitter: bool) -> u64 {
    let exponential_delay = (base_ms as f64 * 1.15_f64.powi(attempt as i32)) as u64;
    let delay_ms = exponential_delay.min(max_ms);

    if do_jitter {
        #[cfg(not(target_arch = "wasm32"))]
        {
            (rand::thread_rng().gen_range(0.7..=1.3) * delay_ms as f64) as u64
        }
        /// WASM-compatible version without random jitter
        #[cfg(target_arch = "wasm32")]
        {
            delay_ms
        }
    } else {
        delay_ms
    }
}

#[cfg(feature = "bincode")]
impl Default for DeploymentConfig {
    fn default() -> Self {
        Self {
            chunk_size: DEFAULT_CHUNK_SIZE,
            max_retries: DEFAULT_MAX_RETRIES,
            retry_base_delay_ms: DEFAULT_RETRY_BASE_DELAY_MS,
            retry_max_delay_ms: DEFAULT_RETRY_MAX_DELAY_MS,
            confirmation_batch_size: DEFAULT_CONFIRMATION_BATCH_SIZE,
        }
    }
}

/// Represents a program deployment operation on the Rialo blockchain.
///
/// This structure encapsulates all the necessary information and methods to deploy
/// a program using either Loader V4 (for EBPF programs) or RISC-V Loader (for RISC-V programs).
#[cfg(feature = "bincode")]
#[derive(Debug)]
pub struct ProgramDeployment<D: ProgramDataSource> {
    /// Program data source that provides the binary data to deploy
    data_source: D,
    /// Program keypair for predictable program ID. By default a generated keypair.
    pub program_keypair: Keypair,
    /// Optional authority. If None, the payer will be the authority
    pub authority: Option<Pubkey>,
    /// Program data (loaded from data source)
    program_data: Option<Vec<u8>>,
    /// Deployment configuration settings
    config: DeploymentConfig,
    /// Loader type to use for deployment (defaults to LoaderV4)
    pub loader_type: LoaderType,
}

#[cfg(feature = "bincode")]
impl<D: ProgramDataSource> ProgramDeployment<D> {
    /// Creates a new program deployment configuration.
    ///
    /// # Arguments
    /// * `data_source` - Program data source (can be a path, memory data, etc.)
    ///
    /// # Examples
    /// ```rust,no_run
    /// use rialo_cdk::program::{ProgramDeployment, FileProgramDataSource};
    ///
    /// // From filesystem path
    /// let deployment = ProgramDeployment::new(FileProgramDataSource::new("./target/deploy/my_program.so"));
    /// ```
    pub fn new(data_source: D) -> Self {
        Self {
            data_source,
            program_keypair: Keypair::new(),
            authority: None,
            program_data: None,
            config: DeploymentConfig::default(),
            loader_type: LoaderType::default(),
        }
    }

    /// Sets the program keypair for predictable deployment.
    pub fn with_program_keypair(mut self, program_keypair: Keypair) -> Self {
        self.program_keypair = program_keypair;
        self
    }

    /// Sets the authority for the program.
    ///
    /// If not set, the transaction payer will be the authority.
    pub fn with_authority(mut self, authority: Pubkey) -> Self {
        self.authority = Some(authority);
        self
    }

    /// Sets custom deployment configuration.
    ///
    /// # Arguments
    /// * `config` - Custom deployment configuration
    ///
    /// # Examples
    /// ```rust,no_run
    /// use rialo_cdk::program::{ProgramDeployment, DeploymentConfig, FileProgramDataSource};
    ///
    /// let config = DeploymentConfig {
    ///     chunk_size: 1200,
    ///     max_retries: 50,
    ///     retry_base_delay_ms: 500,
    ///     retry_max_delay_ms: 30000,
    ///     confirmation_batch_size: 20,
    /// };
    ///
    /// let deployment = ProgramDeployment::new(FileProgramDataSource::new("./program.so"))
    ///     .with_config(config);
    /// ```
    pub fn with_config(mut self, config: DeploymentConfig) -> Self {
        self.config = config;
        self
    }

    /// Sets the loader type for deployment.
    ///
    /// # Arguments
    /// * `loader_type` - The loader type (LoaderV4 or RiscV)
    ///
    /// # Examples
    /// ```rust,no_run
    /// use rialo_cdk::program::{ProgramDeployment, LoaderType, FileProgramDataSource};
    ///
    /// // Deploy a RISC-V program
    /// let deployment = ProgramDeployment::new(FileProgramDataSource::new("./program.elf"))
    ///     .with_loader_type(LoaderType::RiscV);
    /// ```
    pub fn with_loader_type(mut self, loader_type: LoaderType) -> Self {
        self.loader_type = loader_type;
        self
    }

    /// Loads the program data from the data source.
    ///
    /// This method reads the binary program data, validates it, and auto-detects the loader type
    /// based on the program format (PVM bytecode uses RISC-V loader).
    pub fn load_program_data(&mut self) -> Result<&Vec<u8>> {
        if self.program_data.is_none() {
            let data = self.data_source.load_program_data()?;

            // Auto-detect loader type based on magic bytes if not explicitly set by user
            // PVM bytecode (magic: "PVM\0") uses RISC-V loader
            if data.len() >= 4 && &data[0..4] == b"PVM\0" {
                self.loader_type = LoaderType::RiscV;
            }

            self.program_data = Some(data);
        }

        Ok(self.program_data.as_ref().unwrap())
    }

    /// Returns a description of the data source being used.
    ///
    /// This is useful for debugging and logging purposes.
    pub fn data_source_description(&self) -> String {
        self.data_source.description()
    }

    /// Deploys the program to the Rialo blockchain using Loader V4.
    ///
    /// The deployment process follows these steps:
    /// 1. Create and initialize the program buffer account
    /// 2. Write program data in chunks (configurable size per transaction)
    /// 3. Finalize deployment by making the program executable
    ///
    /// # Arguments
    /// * `client` - RPC client to communicate with the blockchain
    /// * `payer` - Wallet to pay for the deployment transactions
    ///
    /// # Returns
    /// The program ID of the deployed program
    ///
    /// # Errors
    /// Returns an error if:
    /// - Program file cannot be read or is invalid
    /// - Payer has insufficient funds
    /// - Network communication fails
    /// - Deployment transactions fail
    #[cfg(feature = "bincode")]
    pub async fn deploy<C: RpcClient>(&mut self, client: &C, payer: &Keyring) -> Result<Pubkey> {
        // Load program data - same as working implementation
        let program_data = self.load_program_data()?.clone();

        let program_pubkey = self.program_keypair.pubkey();

        tracing::info!(program_pubkey = %program_pubkey, "Program will be deployed");
        tracing::info!(data_size = program_data.len(), "Program data loaded");

        // Get config hash prefix for transaction replay protection
        let config_hash_prefix = client.get_config_hash_prefix().await?;

        // Convert payer keyring to Solana SDK keypair format
        let payer_solana_keypair = convert_keyring_to_solana_keypair(payer)?;
        let payer_pubkey = payer_solana_keypair.pubkey();

        // Get rent-exempt amount - same as working implementation
        let buffer_balance = client
            .get_minimum_balance_for_rent_exemption(program_data.len() as u64)
            .await?;
        let buffer_balance_amount = self.buffer_balance_amount(buffer_balance);

        // —— Tx #1: create/init or prepare existing buffer —— //
        // Use exactly same approach as working implementation
        let program_id_cdk = Pubkey::try_from(program_pubkey.to_bytes().as_slice()).unwrap();
        let loader_id_str = self.get_loader_id_string();

        match client.get_account_info(&program_id_cdk).await {
            // If the program account already exists, prepare it for a redeploy by
            // retracting, funding to rent-exempt, and resizing.
            Ok(existing) => {
                // Validate owner
                if existing.owner.to_string() != loader_id_str {
                    let loader_name = match self.loader_type {
                        LoaderType::LoaderV4 => "Loader V4",
                        LoaderType::RiscV => "RISC-V Loader",
                        LoaderType::RexWasmV1 => "RexWasmV1 Loader",
                    };
                    return Err(RialoError::Rpc(
                        crate::error::RpcErrorDetails::from_message(format!(
                            "Existing account is not owned by {}",
                            loader_name
                        )),
                    ));
                }

                // Build instructions
                let mut ixs = Vec::new();

                // Top up if needed to reach rent exemption for new size
                if existing.kelvin < buffer_balance_amount {
                    let deficit = buffer_balance_amount.saturating_sub(existing.kelvin);
                    if deficit > 0 {
                        ixs.push(rialo_s_sdk::system_instruction::transfer(
                            &payer_pubkey,
                            &program_pubkey,
                            deficit,
                        ));
                    }
                }

                // If executable, retract to enter maintenance mode (makes it writable)
                if existing.executable {
                    let retract_ix = match self.loader_type {
                        LoaderType::LoaderV4 => {
                            loader_v4_instruction::retract(&program_pubkey, &payer_pubkey)
                        }
                        LoaderType::RiscV => {
                            RiscVLoaderInstruction::retract(&program_pubkey, &payer_pubkey)
                        }
                        LoaderType::RexWasmV1 => {
                            rex_wasm_v1_instruction::retract(&program_pubkey, &payer_pubkey)
                        }
                    };
                    ixs.push(retract_ix);
                }

                // Ensure correct length for the new executable
                let set_length_ix = match self.loader_type {
                    LoaderType::LoaderV4 => {
                        rialo_s_loader_v4_interface::instruction::set_program_length(
                            &program_pubkey,
                            &payer_pubkey,
                            program_data.len() as u32,
                            &payer_pubkey,
                        )
                    }
                    LoaderType::RiscV => RiscVLoaderInstruction::set_program_length(
                        &program_pubkey,
                        &payer_pubkey,
                        program_data.len() as u32,
                        &payer_pubkey,
                    ),
                    LoaderType::RexWasmV1 => rex_wasm_v1_instruction::set_program_length(
                        &program_pubkey,
                        &payer_pubkey,
                        program_data.len() as u32,
                        &payer_pubkey,
                    ),
                };
                ixs.push(set_length_ix);

                if !ixs.is_empty() {
                    let valid_from = SystemTime::now()
                        .duration_since(UNIX_EPOCH)
                        .expect("Failed to get current time")
                        .as_millis()
                        .try_into()
                        .unwrap_or_default();

                    let tx = rialo_s_sdk::transaction::Transaction::new_signed_with_payer(
                        &ixs,
                        Some(&payer_pubkey),
                        &[&payer_solana_keypair],
                        valid_from,
                        config_hash_prefix,
                        false,
                    );
                    let tx_bytes = bincode::serialize(&tx).map_err(|e| {
                        RialoError::InvalidInput(format!("Failed to serialize transaction: {e}"))
                    })?;
                    let sig = client.send_transaction(&tx_bytes, None).await?;
                    tracing::info!(signature = %sig, "Program buffer prepared for redeploy");

                    // Wait for the retract/resize transaction to be confirmed before writing chunks
                    // Without this, writes may fail if the program hasn't been retracted yet
                    self.wait_for_transaction_confirmation(client, &sig).await?;
                }
            }
            // Account does not exist: create & init the buffer.
            Err(_) => {
                let create_program_ixs = match self.loader_type {
                    LoaderType::LoaderV4 => rialo_s_sdk::loader_v4::create_buffer(
                        &payer_pubkey,
                        &program_pubkey,
                        buffer_balance_amount,
                        &payer_pubkey, // authority
                        program_data.len() as u32,
                        &payer_pubkey, // authority
                    ),
                    LoaderType::RiscV => RiscVLoaderInstruction::create_buffer(
                        &payer_pubkey,
                        &program_pubkey,
                        buffer_balance_amount,
                        &payer_pubkey, // authority
                        program_data.len() as u32,
                        &payer_pubkey, // authority
                    ),
                    LoaderType::RexWasmV1 => rex_wasm_v1_instruction::create_buffer(
                        &payer_pubkey,
                        &program_pubkey,
                        buffer_balance_amount,
                        &payer_pubkey, // authority
                        program_data.len() as u32,
                        &payer_pubkey, // authority
                    ),
                };

                let valid_from = SystemTime::now()
                    .duration_since(UNIX_EPOCH)
                    .expect("Failed to get current time")
                    .as_millis()
                    .try_into()
                    .unwrap_or_default();

                let create_program_tx =
                    rialo_s_sdk::transaction::Transaction::new_signed_with_payer(
                        &create_program_ixs,
                        Some(&payer_pubkey),
                        &[&payer_solana_keypair, &self.program_keypair], // Same as working implementation
                        valid_from,
                        config_hash_prefix,
                        false,
                    );

                // Use the working RPC client's transaction sending approach
                let create_tx_bytes = bincode::serialize(&create_program_tx).map_err(|e| {
                    RialoError::InvalidInput(format!("Failed to serialize transaction: {e}"))
                })?;
                let sig = client.send_transaction(&create_tx_bytes, None).await?;
                tracing::info!(signature = %sig, "Program buffer created");
            }
        }

        // Wait for buffer account to be ready with expected size (header + program data)
        self.wait_for_buffer_ready_solana(client, &program_pubkey, program_data.len())
            .await?;

        // —— Tx #2…#N: write each chunk into the buffer with batched confirmation —— //
        let chunks: Vec<_> = program_data.chunks(self.config.chunk_size).collect();
        let total_chunks = chunks.len();
        let batch_size = self.config.confirmation_batch_size;

        tracing::info!(
            total_chunks,
            batch_size,
            "Writing program chunks with batched confirmation"
        );

        let mut batch_signatures = Vec::with_capacity(batch_size);

        // Extract config values to avoid capturing `self` in futures
        let max_retries = self.config.max_retries;
        let retry_base_delay_ms = self.config.retry_base_delay_ms;
        let retry_max_delay_ms = self.config.retry_max_delay_ms;

        for (i, chunk) in chunks.iter().enumerate() {
            use tracing::Instrument;

            let span = tracing::info_span!("write_chunk", chunk_index = i);
            let sig = async {
                let offset = (i * self.config.chunk_size) as u32;
                let write_ix = match self.loader_type {
                    LoaderType::LoaderV4 => rialo_s_sdk::loader_v4::write(
                        &program_pubkey,
                        &payer_pubkey, // must sign as authority
                        offset,
                        chunk.to_vec(),
                    ),
                    LoaderType::RiscV => RiscVLoaderInstruction::write(
                        &program_pubkey,
                        &payer_pubkey, // must sign as authority
                        offset,
                        chunk.to_vec(),
                    ),
                    LoaderType::RexWasmV1 => rex_wasm_v1_instruction::write(
                        &program_pubkey,
                        &payer_pubkey, // must sign as authority
                        offset,
                        chunk.to_vec(),
                    ),
                };

                let valid_from = SystemTime::now()
                    .duration_since(UNIX_EPOCH)
                    .expect("Failed to get current time")
                    .as_millis()
                    .try_into()
                    .unwrap_or_default();

                let tx = rialo_s_sdk::transaction::Transaction::new_signed_with_payer(
                    &[write_ix],
                    Some(&payer_pubkey),
                    &[&payer_solana_keypair],
                    valid_from,
                    config_hash_prefix,
                    false,
                );

                let tx_bytes = bincode::serialize(&tx).map_err(|e| {
                    RialoError::InvalidInput(format!("Failed to serialize transaction: {e}"))
                })?;
                let sig = client.send_transaction(&tx_bytes, None).await?;
                tracing::info!(chunk_index = i, signature = %sig, "Program chunk written");
                Ok::<_, RialoError>(sig)
            }
            .instrument(span)
            .await?;

            batch_signatures.push(sig);

            // Confirm batch when full or on last chunk
            let is_batch_complete = batch_signatures.len() >= batch_size;
            let is_last_chunk = i == total_chunks - 1;

            if is_batch_complete || is_last_chunk {
                let batch_num = i / batch_size;
                tracing::info!(
                    batch_num,
                    batch_size = batch_signatures.len(),
                    "Confirming chunk batch"
                );

                let confirmation_futures: Vec<_> = batch_signatures
                    .iter()
                    .enumerate()
                    .map(|(batch_idx, sig)| {
                        Self::wait_for_chunk_processed(
                            client,
                            *sig,
                            batch_num * batch_size + batch_idx,
                            max_retries,
                            retry_base_delay_ms,
                            retry_max_delay_ms,
                        )
                    })
                    .collect();

                future::try_join_all(confirmation_futures).await?;
                batch_signatures.clear();
            }
        }

        // —— Final transactions, copy from buffer to program account and deploy —— //

        // Use the appropriate deploy instruction based on loader type
        let deploy_ix = match self.loader_type {
            LoaderType::LoaderV4 => loader_v4_instruction::deploy(&program_pubkey, &payer_pubkey),
            LoaderType::RiscV => RiscVLoaderInstruction::deploy(&program_pubkey, &payer_pubkey),
            LoaderType::RexWasmV1 => {
                rex_wasm_v1_instruction::deploy(&program_pubkey, &payer_pubkey)
            }
        };
        let finalize_ixs = vec![deploy_ix];
        let valid_from = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("Failed to get current time")
            .as_millis()
            .try_into()
            .unwrap_or_default();
        let finalize_tx = rialo_s_sdk::transaction::Transaction::new_signed_with_payer(
            &finalize_ixs,
            Some(&payer_pubkey),
            &[&payer_solana_keypair],
            valid_from,
            config_hash_prefix,
            false,
        );

        let finalize_tx_bytes = bincode::serialize(&finalize_tx).map_err(|e| {
            RialoError::InvalidInput(format!("Failed to serialize transaction: {e}"))
        })?;
        let sig = client.send_transaction(&finalize_tx_bytes, None).await?;
        tracing::info!(signature = %sig, "Program deployment executed");

        // Wait for confirmation like working implementation
        self.wait_for_transaction_confirmation(client, &sig).await?;
        tracing::info!(program_pubkey = %program_pubkey, "Program deployment completed");

        Ok(program_pubkey)
    }

    /// Deploys the program to the Rialo blockchain using Loader V4.
    ///
    /// **Note**: This version returns an error because the `bincode` feature is not enabled.
    /// Program deployment requires the `bincode` feature to be enabled in `Cargo.toml`.
    #[cfg(not(feature = "bincode"))]
    pub async fn deploy<C: RpcClient>(&mut self, _client: &C, _payer: &Wallet) -> Result<Pubkey> {
        Err(RialoError::SerializationError(
            "Program deployment requires the 'bincode' feature to be enabled. Please enable it in your Cargo.toml".to_string(),
        ))
    }

    /// Calculate the buffer balance amount with safety factor.
    ///
    /// This follows the pattern from crates/rialo which uses a 1.2x buffer factor
    /// to avoid running into shortage of funds errors.
    #[cfg(feature = "bincode")]
    fn buffer_balance_amount(&self, balance: u64) -> u64 {
        (balance as f64 * BUFFER_BALANCE_FACTOR) as u64
    }

    /// Get the loader program ID for the current loader type.
    #[cfg(feature = "bincode")]
    fn get_loader_id(&self) -> rialo_s_sdk::pubkey::Pubkey {
        match self.loader_type {
            LoaderType::LoaderV4 => rialo_s_sdk::loader_v4::id(),
            LoaderType::RiscV => rialo_risc_v_loader_interface::id(),
            LoaderType::RexWasmV1 => rialo_rex_wasm_v1_loader_interface::ID,
        }
    }

    /// Get the loader ID as a string for the current loader type.
    #[cfg(feature = "bincode")]
    fn get_loader_id_string(&self) -> String {
        self.get_loader_id().to_string()
    }

    /// Wait for the buffer account to be ready (owned by the appropriate loader and correct size).
    #[cfg(feature = "bincode")]
    async fn wait_for_buffer_ready_solana<C: RpcClient>(
        &self,
        client: &C,
        program_pubkey: &rialo_s_sdk::pubkey::Pubkey,
        program_data_len: usize,
    ) -> Result<()> {
        let mut retry_count = 0;

        let program_id_cdk = Pubkey::try_from(program_pubkey.to_bytes().as_slice()).unwrap();
        let loader_id = self.get_loader_id_string();

        // Calculate expected buffer size: header + program data
        let header_size = match self.loader_type {
            LoaderType::LoaderV4 => rialo_s_sdk::loader_v4::LoaderV4State::program_data_offset(),
            LoaderType::RiscV => {
                rialo_risc_v_loader_interface::RiscVLoaderState::program_data_offset()
            }
            LoaderType::RexWasmV1 => {
                rialo_rex_wasm_v1_loader_interface::state::RexWasmV1LoaderState::program_data_offset(
                )
            }
        };
        let expected_size = header_size + program_data_len;

        tracing::info!(program_pubkey = %program_pubkey, expected_size, "Waiting for buffer account to be ready");

        loop {
            match client.get_account_info(&program_id_cdk).await {
                Ok(account_info) => {
                    if account_info.owner.to_string() == loader_id {
                        if account_info.space as usize >= expected_size {
                            tracing::info!(
                                retry_count,
                                space = account_info.space,
                                "Program account ready"
                            );
                            return Ok(());
                        }
                        tracing::debug!(
                            retry_count,
                            actual_size = account_info.space,
                            expected_size,
                            "Buffer has correct owner but wrong size, continuing to wait"
                        );
                    }
                    // Continue waiting if wrong owner or wrong size
                }
                Err(_) => {
                    // Account doesn't exist yet, continue waiting
                }
            }

            if retry_count >= self.config.max_retries {
                return Err(RialoError::Rpc(
                    crate::error::RpcErrorDetails::from_message(format!(
                        "Timed out waiting for buffer account after {} attempts",
                        self.config.max_retries
                    )),
                ));
            }

            #[cfg(all(feature = "bincode", not(target_arch = "wasm32")))]
            {
                let delay_ms = calculate_backoff(
                    retry_count,
                    self.config.retry_base_delay_ms,
                    self.config.retry_max_delay_ms,
                    true,
                );
                sleep(Duration::from_millis(delay_ms)).await;
            }
            retry_count += 1;
        }
    }

    /// Wait for a chunk transaction to be processed using signature status polling.
    /// Standalone function to allow concurrent execution with `futures::try_join_all`.
    #[cfg(feature = "bincode")]
    async fn wait_for_chunk_processed<C: RpcClient>(
        client: &C,
        signature: crate::rpc::types::Signature,
        chunk_index: usize,
        max_retries: u32,
        retry_base_delay: u64,
        retry_max_delay: u64,
    ) -> Result<()> {
        let mut retry_count = 0;
        let signatures = vec![signature];

        loop {
            let last_error;

            match client.get_signature_statuses(&signatures).await {
                Ok(statuses) => {
                    if let Some(status) = statuses.first().and_then(|s| s.as_ref()) {
                        if let Some(err) = &status.err {
                            return Err(RialoError::Transaction(format!(
                                "Chunk {chunk_index} (signature: {signature}) failed: {err}"
                            )));
                        }
                        // Transaction succeeded - wait for execution
                        if status.executed {
                            // Transaction has been executed
                            tracing::debug!(chunk_index, signature = %signature,
                                slot = status.slot, "Chunk write executed");
                            return Ok(());
                        }

                        last_error = "Chunk processed but waiting for confirmation".to_string();
                        tracing::debug!(chunk_index, signature = %signature, last_error);
                    } else {
                        last_error = "Transaction not yet processed".to_string();
                        tracing::debug!(chunk_index, signature = %signature, last_error);
                    }
                }
                Err(e) => {
                    last_error = format!("Failed to get signature status: {e}");
                    tracing::debug!(chunk_index, signature = %signature, error = ?e, last_error);
                }
            }

            if retry_count >= max_retries {
                return Err(RialoError::Rpc(
                    crate::error::RpcErrorDetails::from_message(format!(
                        "Chunk {chunk_index} timed out after {max_retries} attempts: {last_error}"
                    )),
                ));
            }

            #[cfg(not(target_arch = "wasm32"))]
            {
                let delay_ms =
                    calculate_backoff(retry_count, retry_base_delay, retry_max_delay, true);
                sleep(Duration::from_millis(delay_ms)).await;
            }
            retry_count += 1;
        }
    }

    /// Wait for transaction confirmation using signature status polling.
    ///
    /// This method polls the transaction signature status until it's confirmed
    /// or reaches the maximum retry limit.
    #[cfg(feature = "bincode")]
    async fn wait_for_transaction_confirmation<C: RpcClient>(
        &self,
        client: &C,
        signature: &crate::rpc::types::Signature,
    ) -> Result<()> {
        let mut retry_count = 0;
        let signatures = vec![*signature];

        tracing::info!(signature = %signature, "Waiting for transaction confirmation");

        loop {
            let last_error;

            match client.get_signature_statuses(&signatures).await {
                Ok(statuses) => {
                    if let Some(status) = statuses.first().and_then(|s| s.as_ref()) {
                        // Transaction found and processed
                        if let Some(err) = &status.err {
                            return Err(RialoError::Transaction(format!(
                                "Transaction failed with error: {err}"
                            )));
                        }

                        // Transaction succeeded - wait for execution
                        if status.executed {
                            // Transaction has been executed
                            tracing::info!(
                                signature = %signature,
                                slot = status.slot,
                                "Transaction executed"
                            );
                            return Ok(());
                        }

                        last_error = "Transaction processed but waiting for confirmation";
                        tracing::debug!(
                            signature = %signature,
                            slot = status.slot,
                            executed = status.executed,
                            last_error
                        );
                    } else {
                        last_error = "Transaction not yet processed";
                        tracing::debug!(signature = %signature, last_error);
                    }
                }
                Err(e) => {
                    last_error = "Failed to get signature status";
                    tracing::debug!(signature = %signature, error = ?e, last_error);
                }
            }

            if retry_count >= self.config.max_retries {
                return Err(RialoError::Rpc(
                    crate::error::RpcErrorDetails::from_message(format!(
                        "Timed out waiting for transaction confirmation after {} attempts: {last_error}",
                        self.config.max_retries
                    )),
                ));
            }

            #[cfg(all(feature = "bincode", not(target_arch = "wasm32")))]
            {
                let delay_ms = calculate_backoff(
                    retry_count,
                    self.config.retry_base_delay_ms,
                    self.config.retry_max_delay_ms,
                    true,
                );
                sleep(Duration::from_millis(delay_ms)).await;
            }
            retry_count += 1;
        }
    }
}

/// Builder pattern for creating program deployments.
///
/// This provides a fluent interface for configuring program deployments.
#[cfg(feature = "bincode")]
pub struct ProgramDeploymentBuilder<D: ProgramDataSource> {
    deployment: ProgramDeployment<D>,
}

#[cfg(feature = "bincode")]
impl<D: ProgramDataSource> ProgramDeploymentBuilder<D> {
    /// Creates a new program deployment builder.
    pub fn new(data_source: D) -> Self {
        Self {
            deployment: ProgramDeployment::new(data_source),
        }
    }

    /// Sets the program keypair for deployment.
    pub fn with_program_keypair(mut self, program_keypair: Keypair) -> Self {
        self.deployment = self.deployment.with_program_keypair(program_keypair);
        self
    }

    /// Sets the authority for the program.
    pub fn with_authority(mut self, authority: Pubkey) -> Self {
        self.deployment = self.deployment.with_authority(authority);
        self
    }

    /// Sets custom deployment configuration.
    pub fn with_config(mut self, config: DeploymentConfig) -> Self {
        self.deployment = self.deployment.with_config(config);
        self
    }

    /// Builds the program deployment configuration.
    pub fn build(self) -> ProgramDeployment<D> {
        self.deployment
    }
}

#[cfg(test)]
mod tests {
    use crate::program::{deployment::calculate_backoff, DeploymentConfig};

    #[test]
    #[cfg(feature = "bincode")]
    fn five_mins_ms_retry() {
        let config = DeploymentConfig::default();
        let five_mins_ms = 5 * 60 * 1000;

        // without jitter
        let sum_ms: u64 = (0..config.max_retries)
            .map(|i| {
                calculate_backoff(
                    i,
                    config.retry_base_delay_ms,
                    config.retry_max_delay_ms,
                    false,
                )
            })
            .sum();

        assert!(
            sum_ms > five_mins_ms,
            "Expected sum_ms = {sum_ms}ms of retry delays to be greater than 5 minutes ({five_mins_ms}ms)"
        );

        // with jitter
        for _ in 0..1000 {
            let sum_ms: u64 = (0..config.max_retries)
                .map(|i| {
                    calculate_backoff(
                        i,
                        config.retry_base_delay_ms,
                        config.retry_max_delay_ms,
                        true,
                    )
                })
                .sum();

            assert!(
                sum_ms > five_mins_ms,
                "Expected sum_ms = {sum_ms}ms of retry delays to be greater than 5 minutes ({five_mins_ms}ms)"
            );
        }
    }
}
