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

//! Program invocation functionality for the Rialo blockchain.
//!
//! This module provides the ability to invoke deployed programs on the Rialo blockchain,
//! following the patterns established in the Solana SDK from `sdk/`.

#[cfg(feature = "borsh")]
use borsh;
#[cfg(feature = "bincode")]
use serde::Serialize;

use crate::{
    constants::ConfigHashPrefix,
    error::{Result, RialoError},
    keyring::Keyring,
    rpc::{types::Pubkey, RpcClient},
    transaction::{AccountMeta, Instruction, TransactionBuilder},
};

/// Represents a program invocation operation on the Rialo blockchain.
///
/// This structure encapsulates all the necessary information to invoke a deployed
/// program, following the instruction patterns from the Solana SDK.
#[derive(Debug, Clone)]
pub struct ProgramInvocation {
    /// The program ID to invoke
    pub program_id: Pubkey,
    /// Accounts required for the instruction
    pub accounts: Vec<AccountMeta>,
    /// Instruction data to pass to the program
    pub data: Vec<u8>,
}

impl ProgramInvocation {
    /// Creates a new program invocation.
    ///
    /// # Arguments
    /// * `program_id` - The public key of the program to invoke
    ///
    /// # Examples
    /// ```rust,no_run
    /// use rialo_cdk::{program::ProgramInvocation, rpc::types::Pubkey};
    /// use std::str::FromStr;
    ///
    /// let program_id = Pubkey::from_str("MyProgram1111111111111111111111111111111").unwrap();
    /// let invocation = ProgramInvocation::new(program_id);
    /// ```
    pub fn new(program_id: Pubkey) -> Self {
        Self {
            program_id,
            accounts: Vec::new(),
            data: Vec::new(),
        }
    }

    /// Adds an account to the instruction.
    ///
    /// # Arguments
    /// * `pubkey` - The account's public key
    /// * `is_signer` - Whether this account must sign the transaction
    /// * `is_writable` - Whether this account will be modified
    pub fn add_account(mut self, pubkey: Pubkey, is_signer: bool, is_writable: bool) -> Self {
        self.accounts.push(AccountMeta {
            pubkey,
            is_signer,
            is_writable,
        });
        self
    }

    /// Adds a read-only account to the instruction.
    pub fn add_readonly_account(self, pubkey: Pubkey) -> Self {
        self.add_account(pubkey, false, false)
    }

    /// Adds a writable account to the instruction.
    pub fn add_writable_account(self, pubkey: Pubkey) -> Self {
        self.add_account(pubkey, false, true)
    }

    /// Adds a signer account to the instruction.
    pub fn add_signer_account(self, pubkey: Pubkey, is_writable: bool) -> Self {
        self.add_account(pubkey, true, is_writable)
    }

    /// Sets the instruction data using raw bytes.
    pub fn with_data(mut self, data: Vec<u8>) -> Self {
        self.data = data;
        self
    }

    /// Sets the instruction data using a serializable type with Borsh encoding.
    ///
    /// This follows the pattern from the Solana SDK where instructions are commonly
    /// encoded using Borsh serialization.
    #[cfg(feature = "borsh")]
    pub fn with_borsh_data<T>(mut self, data: &T) -> Result<Self>
    where
        T: borsh::BorshSerialize,
    {
        self.data = borsh::to_vec(data).map_err(|e| {
            RialoError::SerializationError(format!("Failed to serialize instruction data: {}", e))
        })?;
        Ok(self)
    }

    /// Sets the instruction data using a serializable type with Bincode encoding.
    #[cfg(feature = "bincode")]
    pub fn with_bincode_data<T>(mut self, data: &T) -> Result<Self>
    where
        T: Serialize,
    {
        self.data = bincode::serialize(data).map_err(|e| {
            RialoError::SerializationError(format!("Failed to serialize instruction data: {e}"))
        })?;
        Ok(self)
    }

    /// Converts this program invocation into an instruction.
    pub fn into_instruction(self) -> Instruction {
        Instruction {
            program_id: self.program_id,
            accounts: self.accounts,
            data: self.data,
        }
    }

    /// Invokes the program by sending a transaction to the blockchain.
    ///
    /// # Arguments
    /// * `client` - RPC client to communicate with the blockchain
    /// * `payer` - Wallet to pay for the transaction
    ///
    /// # Returns
    /// The transaction signature
    ///
    /// # Examples
    /// ```rust,no_run
    /// use rialo_cdk::{
    ///     program::ProgramInvocation,
    ///     rpc::{HttpRpcClient, types::Pubkey},
    ///     wallet::traits::Wallet,
    /// };
    /// use ed25519_dalek::SigningKey as Keypair;
    /// use std::str::FromStr;
    ///
    /// # async fn example() -> rialo_cdk::Result<()> {
    /// let client = HttpRpcClient::new("https://api.devnet.rialo.xyz".to_string());
    /// let keypair = Keypair::generate(&mut rand::thread_rng());
    /// let wallet = Wallet::new("test".to_string(), keypair, None, None);
    /// let program_id = Pubkey::from_str("MyProgram1111111111111111111111111111111").unwrap();
    ///
    /// let signature = ProgramInvocation::new(program_id)
    ///     .add_signer_account(wallet.pubkey(), true)
    ///     .with_data(vec![1, 2, 3]) // Custom instruction data
    ///     .invoke(&client, &wallet)
    ///     .await?;
    ///
    /// println!("Program invoked with signature: {}", signature);
    /// # Ok(())
    /// # }
    /// ```
    pub async fn invoke<C: RpcClient>(self, client: &C, payer: &Keyring) -> Result<String> {
        // Get current time in milliseconds for valid_from
        let valid_from = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("Time went backwards")
            .as_millis() as i64;

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

        // Convert to instruction
        let instruction = self.into_instruction();

        // Build transaction
        let mut tx_builder =
            TransactionBuilder::new(payer.pubkey(), valid_from, config_hash_prefix);
        tx_builder.add_instruction(instruction);
        let signed_tx = tx_builder.sign_with_keypair(payer, 0)?;

        // Send transaction
        let signature = client.send_transaction(&signed_tx, None).await?;
        Ok(signature.to_string())
    }

    /// Simulates the program invocation without submitting to the blockchain.
    ///
    /// This is useful for testing and validation before actual execution.
    pub async fn simulate(self, payer: &Keyring) -> Result<()> {
        // Get current time in milliseconds for valid_from
        let valid_from = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("Time went backwards")
            .as_millis() as i64;

        // Do basic validation before converting
        if self.data.is_empty() {
            return Err(RialoError::InvalidInput(
                "Instruction data is empty".to_string(),
            ));
        }

        // Use a default config_hash_prefix for simulation (not connected to a network)
        let config_hash_prefix = ConfigHashPrefix::new(0);

        // Convert to instruction
        let instruction = self.into_instruction();

        // Build transaction
        let mut tx_builder =
            TransactionBuilder::new(payer.pubkey(), valid_from, config_hash_prefix);
        tx_builder.add_instruction(instruction);
        let _signed_tx = tx_builder.sign_with_keypair(payer, 0)?;

        // Additional validation could be added here
        println!("Program invocation simulation passed");
        Ok(())
    }
}

/// Builder pattern for creating program invocations.
///
/// This provides a fluent interface for configuring program invocations with
/// common patterns and helper methods.
pub struct ProgramInvocationBuilder {
    invocation: ProgramInvocation,
}

impl ProgramInvocationBuilder {
    /// Creates a new program invocation builder.
    pub fn new(program_id: Pubkey) -> Self {
        Self {
            invocation: ProgramInvocation::new(program_id),
        }
    }

    /// Adds an account to the instruction.
    pub fn add_account(mut self, pubkey: Pubkey, is_signer: bool, is_writable: bool) -> Self {
        self.invocation = self.invocation.add_account(pubkey, is_signer, is_writable);
        self
    }

    /// Adds a read-only account.
    pub fn add_readonly_account(mut self, pubkey: Pubkey) -> Self {
        self.invocation = self.invocation.add_readonly_account(pubkey);
        self
    }

    /// Adds a writable account.
    pub fn add_writable_account(mut self, pubkey: Pubkey) -> Self {
        self.invocation = self.invocation.add_writable_account(pubkey);
        self
    }

    /// Adds a signer account.
    pub fn add_signer_account(mut self, pubkey: Pubkey, is_writable: bool) -> Self {
        self.invocation = self.invocation.add_signer_account(pubkey, is_writable);
        self
    }

    /// Sets the instruction data.
    pub fn with_data(mut self, data: Vec<u8>) -> Self {
        self.invocation = self.invocation.with_data(data);
        self
    }

    /// Sets the instruction data using Borsh encoding.
    #[cfg(feature = "borsh")]
    pub fn with_borsh_data<T>(mut self, data: &T) -> Result<Self>
    where
        T: borsh::BorshSerialize,
    {
        self.invocation = self.invocation.with_borsh_data(data)?;
        Ok(self)
    }

    /// Sets the instruction data using Bincode encoding.
    #[cfg(feature = "bincode")]
    pub fn with_bincode_data<T>(mut self, data: &T) -> Result<Self>
    where
        T: Serialize,
    {
        self.invocation = self.invocation.with_bincode_data(data)?;
        Ok(self)
    }

    /// Builds the program invocation.
    pub fn build(self) -> ProgramInvocation {
        self.invocation
    }

    /// Builds and immediately invokes the program.
    pub async fn invoke<C: RpcClient>(self, client: &C, payer: &Keyring) -> Result<String> {
        self.build().invoke(client, payer).await
    }

    /// Builds and simulates the program invocation.
    pub async fn simulate(self, payer: &Keyring) -> Result<()> {
        self.build().simulate(payer).await
    }
}

/// Helper functions for common program invocation patterns.
impl ProgramInvocation {
    /// Creates a program invocation for a simple transfer-like operation.
    ///
    /// This is a common pattern where a program needs a source account (signer + writable),
    /// a destination account (writable), and some instruction data.
    pub fn transfer_like(program_id: Pubkey, from: Pubkey, to: Pubkey, data: Vec<u8>) -> Self {
        Self::new(program_id)
            .add_signer_account(from, true)
            .add_writable_account(to)
            .with_data(data)
    }

    /// Creates a program invocation for an initialization operation.
    ///
    /// Common pattern for initializing program state where an account is created
    /// and configured.
    pub fn initialize(
        program_id: Pubkey,
        initializer: Pubkey,
        account_to_initialize: Pubkey,
        data: Vec<u8>,
    ) -> Self {
        Self::new(program_id)
            .add_signer_account(initializer, true)
            .add_writable_account(account_to_initialize)
            .with_data(data)
    }

    /// Creates a program invocation for a close account operation.
    ///
    /// Common pattern for closing program accounts and recovering rent.
    pub fn close_account(
        program_id: Pubkey,
        authority: Pubkey,
        account_to_close: Pubkey,
        rent_recipient: Pubkey,
    ) -> Self {
        Self::new(program_id)
            .add_signer_account(authority, false)
            .add_writable_account(account_to_close)
            .add_writable_account(rent_recipient)
            .with_data(vec![]) // Close instructions often have no data
    }
}
