use super::*;
pub mod deploy;
pub use deploy::*;
pub mod execute;
pub use execute::*;
pub mod helpers;
pub use helpers::*;
pub mod network;
pub use network::*;
pub mod resolver;
pub use resolver::*;
pub mod transfer;
pub use transfer::*;
#[derive(Clone)]
pub struct ProgramManager<N: Network> {
    pub(crate) programs: IndexMap<ProgramID<N>, Program<N>>,
    pub(crate) private_key: Option<PrivateKey<N>>,
    pub(crate) private_key_ciphertext: Option<Ciphertext<N>>,
    pub(crate) local_program_directory: Option<PathBuf>,
    pub(crate) api_client: Option<AleoAPIClient<N>>,
}
impl<N: Network> ProgramManager<N> {
    pub fn new(
        private_key: Option<PrivateKey<N>>,
        private_key_ciphertext: Option<Ciphertext<N>>,
        api_client: Option<AleoAPIClient<N>>,
        local_program_directory: Option<PathBuf>,
    ) -> Result<Self> {
        if private_key.is_some() && private_key_ciphertext.is_some() {
            bail!("Cannot have both private key and private key ciphertext");
        } else if private_key.is_none() && private_key_ciphertext.is_none() {
            bail!("Must have either private key or private key ciphertext");
        }
        let programs = IndexMap::new();
        Ok(Self { programs, private_key, private_key_ciphertext, local_program_directory, api_client })
    }
    pub fn add_program(&mut self, program: &Program<N>) -> Result<()> {
        if self.contains_program(program.id())? {
            bail!("program already exists")
        };
        self.programs.entry(*program.id()).or_insert(program.clone());
        Ok(())
    }
    pub fn initialize_vm(
        api_client: &AleoAPIClient<N>,
        program: &Program<N>,
        initialize_execution: bool,
    ) -> Result<VM<N, ConsensusMemory<N>>> {
        let store = ConsensusStore::<N, ConsensusMemory<N>>::open(None)?;
        let vm = VM::<N, ConsensusMemory<N>>::from(store)?;
        let credits_id = ProgramID::<N>::from_str("credits.aleo")?;
        api_client.get_program_imports_from_source(program)?.iter().try_for_each(|(_, import)| {
            if import.id() != &credits_id {
                vm.process().write().add_program(import)?
            }
            Ok::<_, Error>(())
        })?;
        if initialize_execution {
            vm.process().write().add_program(program)?;
        }
        Ok(vm)
    }
    pub fn update_program(&mut self, program: &Program<N>) -> Option<Program<N>> {
        self.programs.insert(*program.id(), program.clone())
    }
    pub fn get_program(&self, program_id: impl TryInto<ProgramID<N>>) -> Result<Program<N>> {
        let program_id = program_id.try_into().map_err(|_| anyhow!("invalid program id"))?;
        self.programs.get(&program_id).map_or(Err(anyhow!("program not found")), |program| Ok(program.clone()))
    }
    pub fn contains_program(&self, program_id: impl TryInto<ProgramID<N>>) -> Result<bool> {
        let program_id = program_id.try_into().map_err(|_| anyhow!("invalid program id"))?;
        Ok(self.programs.contains_key(&program_id))
    }
    pub(super) fn get_private_key(&self, password: Option<&str>) -> Result<PrivateKey<N>> {
        if self.private_key.is_none() && self.private_key_ciphertext.is_none() {
            bail!("Private key is not configured");
        };
        if let Some(private_key) = &self.private_key {
            if self.private_key_ciphertext.is_some() {
                bail!(
                    "Private key ciphertext is also configured, cannot have both private key and private key ciphertext"
                );
            }
            return Ok(*private_key);
        };
        if let Some(ciphertext) = &self.private_key_ciphertext {
            if self.private_key.is_some() {
                bail!("Private key is already configured, cannot have both private key and private key ciphertext");
            }
            let password = password.ok_or_else(|| anyhow!("Private key is encrypted, password is required"))?;
            return Encryptor::<N>::decrypt_private_key_with_secret(ciphertext, password);
        };
        bail!("Private key configuration error")
    }
}
#[cfg(test)]
#[cfg(not(feature = "wasm"))]
mod tests {
    use super::*;
    use crate::{
        test_utils::{HELLO_PROGRAM, HELLO_PROGRAM_2},
        RECIPIENT_PRIVATE_KEY,
    };
    #[test]
    fn test_constructors_fail_with_multiple_keys_or_no_keys() {
        let api_client = AleoAPIClient::<Testnet3>::testnet3();
        let private_key = PrivateKey::<Testnet3>::from_str(RECIPIENT_PRIVATE_KEY).unwrap();
        let private_key_ciphertext =
            Encryptor::<Testnet3>::encrypt_private_key_with_secret(&private_key, "password").unwrap();
        let temp_dir = std::env::temp_dir();
        let program_manager =
            ProgramManager::<Testnet3>::new(None, None, Some(api_client.clone()), Some(temp_dir.clone()));
        assert!(program_manager.is_err());
        let program_manager = ProgramManager::<Testnet3>::new(
            Some(private_key),
            Some(private_key_ciphertext.clone()),
            Some(api_client.clone()),
            Some(temp_dir.clone()),
        );
        assert!(program_manager.is_err());
        let program_manager =
            ProgramManager::<Testnet3>::new(Some(private_key), None, Some(api_client.clone()), Some(temp_dir.clone()));
        assert!(program_manager.is_ok());
        let program_manager =
            ProgramManager::<Testnet3>::new(None, Some(private_key_ciphertext), Some(api_client), Some(temp_dir));
        assert!(program_manager.is_ok());
    }
    #[test]
    fn test_program_management_methods() {
        let private_key = PrivateKey::<Testnet3>::from_str(RECIPIENT_PRIVATE_KEY).unwrap();
        let mut program_manager = ProgramManager::<Testnet3>::new(Some(private_key), None, None, None).unwrap();
        let program = Program::<Testnet3>::from_str(HELLO_PROGRAM).unwrap();
        assert!(!program_manager.contains_program(program.id()).unwrap());
        program_manager.add_program(&program).unwrap();
        assert!(program_manager.contains_program(program.id()).unwrap());
        assert_eq!(program_manager.get_program(program.id()).unwrap(), program);
        assert_eq!(program_manager.get_program("hello.aleo").unwrap(), program);
        assert!(program_manager.contains_program("hello.aleo").unwrap());
        assert!(program_manager.add_program(&program).is_err());
        let program_2 = Program::<Testnet3>::from_str(HELLO_PROGRAM_2).unwrap();
        let replaced_program = program_manager.update_program(&program_2).unwrap();
        let retrieved_program = program_manager.get_program(program.id()).unwrap();
        assert_eq!(replaced_program, program);
        assert_eq!(retrieved_program, program_2);
    }
    #[test]
    fn test_private_key_retrieval_from_ciphertext() {
        let private_key = PrivateKey::from_str(RECIPIENT_PRIVATE_KEY).unwrap();
        let private_key_ciphertext =
            Encryptor::<Testnet3>::encrypt_private_key_with_secret(&private_key, "password").unwrap();
        let temp_dir = std::env::temp_dir();
        let api_client = AleoAPIClient::<Testnet3>::testnet3();
        let program_manager =
            ProgramManager::<Testnet3>::new(None, Some(private_key_ciphertext), Some(api_client), Some(temp_dir))
                .unwrap();
        let recovered_private_key = program_manager.get_private_key(Some("password")).unwrap();
        assert_eq!(recovered_private_key, private_key);
        let recovered_private_key = program_manager.get_private_key(Some("wrong_password"));
        assert!(recovered_private_key.is_err());
        let recoverd_private_key = program_manager.get_private_key(None);
        assert!(recoverd_private_key.is_err());
    }
    #[test]
    fn test_private_key_retrieval_from_plaintext() {
        let private_key = PrivateKey::<Testnet3>::from_str(RECIPIENT_PRIVATE_KEY).unwrap();
        let temp_dir = std::env::temp_dir();
        let api_client = AleoAPIClient::<Testnet3>::testnet3();
        let program_manager =
            ProgramManager::<Testnet3>::new(Some(private_key), None, Some(api_client), Some(temp_dir)).unwrap();
        let recovered_private_key = program_manager.get_private_key(None).unwrap();
        assert_eq!(recovered_private_key, private_key);
        let recovered_private_key = program_manager.get_private_key(Some("password")).unwrap();
        assert_eq!(recovered_private_key, private_key);
    }
    #[test]
    fn test_import_resolution() {
        let api_client = AleoAPIClient::<Testnet3>::testnet3();
        let top_level_program = api_client.get_program("imported_add_mul.aleo").unwrap();
        let add_program = api_client.get_program("addition_test.aleo").unwrap();
        let multiply_program = api_client.get_program("multiply_test.aleo").unwrap();
        let double_program = api_client.get_program("double_test.aleo").unwrap();
        let vm_execute = ProgramManager::<Testnet3>::initialize_vm(&api_client, &top_level_program, true).unwrap();
        let vm_deploy = ProgramManager::<Testnet3>::initialize_vm(&api_client, &top_level_program, false).unwrap();
        let top_program = vm_execute.process().read().get_program("imported_add_mul.aleo").unwrap().clone();
        assert_eq!(top_level_program, top_program);
        let add_import = vm_execute.process().read().get_program("addition_test.aleo").unwrap().clone();
        assert_eq!(add_program, add_import);
        let multiply_import = vm_execute.process().read().get_program("multiply_test.aleo").unwrap().clone();
        assert_eq!(multiply_program, multiply_import);
        let double_import = vm_execute.process().read().get_program("double_test.aleo").unwrap().clone();
        assert_eq!(double_program, double_import);
        let top_program = vm_deploy.process().read().get_program("imported_add_mul.aleo").cloned();
        assert!(top_program.is_err());
        let add_import = vm_deploy.process().read().get_program("addition_test.aleo").unwrap().clone();
        assert_eq!(add_program, add_import);
        let multiply_import = vm_deploy.process().read().get_program("multiply_test.aleo").unwrap().clone();
        assert_eq!(multiply_program, multiply_import);
        let double_import = vm_deploy.process().read().get_program("double_test.aleo").unwrap().clone();
        assert_eq!(double_program, double_import);
    }
}