use crate::prelude::*;

use super::program::get_on_demand_program;
use anchor_client::anchor_lang::Event;
use anchor_client::solana_sdk::commitment_config::CommitmentConfig;
use base64::{engine::general_purpose, Engine as _};
use futures::{Future, StreamExt};
use sha2::{Digest, Sha256};
use solana_client::nonblocking::pubsub_client::PubsubClient;
use solana_client::nonblocking::rpc_client::RpcClient as NonblockingRpcClient;
use solana_client::rpc_config::RpcTransactionLogsConfig;
use solana_client::rpc_config::RpcTransactionLogsFilter;
use solana_sdk::client::SyncClient;
use solana_sdk::signer::keypair::{keypair_from_seed, read_keypair_file, Keypair};
use solana_sdk::signer::Signer;
use std::env;
use std::result::Result;
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::RwLock;
use std::fmt::Debug;
use crate::AnchorClient;

pub fn build_tx<A: ToAccountMetas, I: InstructionData + Discriminator>(
    anchor_client: &anchor_client::Client<std::sync::Arc<Keypair>>,
    program_id: &Pubkey,
    accounts: A,
    params: I,
    signers: Vec<&Keypair>,
) -> Result<Transaction, OnDemandError> {
    let payer = signers[0];
    let ix = Instruction {
        program_id: *program_id,
        accounts: accounts.to_account_metas(None),
        data: params.data(),
    };
    let mut tx = Transaction::new_with_payer(&[ix], Some(&payer.pubkey()));
    let program = get_on_demand_program(anchor_client)?;
    let blockhash = program.rpc().get_latest_blockhash().unwrap_or_default();
    tx.try_sign(&signers, blockhash)
        .map_err(|e| OnDemandError::CustomError {
            message: "Failed to sign txn".into(),
            source: std::sync::Arc::new(e),
        })?;
    Ok(tx)
}

pub async fn get_async_rpc(
    client: &Arc<RwLock<AnchorClient>>,
) -> Result<Arc<NonblockingRpcClient>, OnDemandError> {
    let client = client.clone();
    let ro_client = client.read().await;
    let program = get_on_demand_program(&ro_client)?;
    let rpc = program.async_rpc();
    Ok(Arc::new(rpc))
}

pub fn ix_to_tx(
    ixs: &[Instruction],
    signers: &[&Keypair],
    blockhash: solana_program::hash::Hash,
) -> Result<Transaction, OnDemandError> {
    let msg = Message::new(ixs, Some(&signers[0].pubkey()));
    let mut tx = Transaction::new_unsigned(msg);
    // for (i,s) in signers.iter().enumerate() {
    //     tx.try_sign(&signers.to_vec(), blockhash)
    //     .map_err(|e| OnDemandError::CustomError { message: format!("Failed to sign txn", {}), source: std::sync::Arc::new(e) })?;
    // }
    tx.try_sign(&signers.to_vec(), blockhash)
        .map_err(|e| OnDemandError::CustomError {
            message: "Failed to sign txn".into(),
            source: std::sync::Arc::new(e),
        })?;
    Ok(tx)
}

pub async fn get_enclave_signer_pubkey(
    enclave_signer: &Arc<RwLock<Keypair>>,
) -> Result<Arc<Pubkey>, OnDemandError> {
    let enclave_signer = enclave_signer.clone();
    let ro_enclave_signer = enclave_signer.read().await;
    let pubkey = Arc::new(ro_enclave_signer.pubkey());
    Ok(pubkey)
}

pub fn load_env_pubkey(key: &str) -> Result<Pubkey, OnDemandError> {
    Pubkey::from_str(&env::var(key).unwrap_or_default())
        .map_err(|_| OnDemandError::EnvVariableMissing(key.to_string()))
}

/// Parse a string into an optional Pubkey. If the string is empty, return None.
pub fn parse_optional_pubkey(var: &str) -> Option<Pubkey> {
    if var.is_empty() {
        None
    } else {
        match Pubkey::from_str(var) {
            Ok(pubkey) => {
                if pubkey != Pubkey::default() {
                    Some(pubkey)
                } else {
                    None
                }
            }
            Err(_) => None,
        }
    }
}

/// Generates a keypair from a base seed, secret key, optional additional bytes, and an optional program ID.
///
/// # Arguments
///
/// * `base` - The base seed as a string.
/// * `secret_key` - The secret key as a vector of bytes.
/// * `more_bytes` - Optional additional bytes to include in the seed.
/// * `program_id` - Optional program ID to include in the seed.
///
/// # Returns
///
/// Returns a `Result` containing an `Arc<Keypair>` if the keypair is successfully derived, or an `OnDemandError` if there is an error.
///
/// # Errors
///
/// Returns an `OnDemandError` with the message "InvalidSecretKey" if the length of the secret key is not 32 bytes.
/// Returns an `OnDemandError` with the message "Failed to derive keypair" if there is an error deriving the keypair.
///
/// # Example
///
/// ```rust
/// use solana_sdk::pubkey::Pubkey;
/// use solana_sdk::signature::{Keypair, keypair_from_seed};
/// use solana_sdk::hash::Hash;
/// use sha2::{Digest, Sha256};
/// use std::sync::Arc;
/// use switchboard_solana::OnDemandError;
///
/// let base = "base_seed";
/// let secret_key = vec![0; 32];
/// let more_bytes = Some(vec![1, 2, 3]);
/// let program_id = Some(Pubkey::new_unique());
///
/// let result = switchboard_solana::client::utils::keypair_from_base_seed(base, secret_key, more_bytes, program_id);
/// match result {
///     Ok(keypair) => {
///         // Key pair successfully derived
///         println!("Derived keypair: {:?}", keypair);
///     }
///     Err(error) => {
///         // Error deriving key pair
///         println!("Failed to derive keypair: {:?}", error);
///     }
/// }
/// ```
pub fn keypair_from_base_seed(
    base: &str,
    secret_key: Vec<u8>,
    more_bytes: Option<Vec<u8>>,
    program_id: Option<Pubkey>,
) -> Result<Arc<Keypair>, OnDemandError> {
    if secret_key.len() != 32 {
        return Err(OnDemandError::Message("InvalidSecretKey"));
    }

    let mut seed = base.as_bytes().to_vec();
    seed.extend_from_slice(&secret_key);

    if let Some(bytes) = more_bytes.as_ref() {
        seed.extend_from_slice(bytes);
    }

    // Optionally, allow the progam ID to be included in the bytes so we
    // can create new environments on different program IDs without collisions.
    if let Some(program_id) = program_id.as_ref() {
        seed.extend_from_slice(&program_id.try_to_vec().unwrap_or_default());
    } else {
        seed.extend_from_slice(
            &*SWITCHBOARD_ATTESTATION_PROGRAM_ID
                .try_to_vec()
                .unwrap_or_default(),
        );
    }

    match keypair_from_seed(&Sha256::digest(&seed)) {
        Ok(keypair) => Ok(Arc::new(keypair)),
        Err(e) => {
            if let Some(err) = e.source() {
                println!("Failed to derive keypair -- {}", err);
            }

            Err(OnDemandError::Message("Failed to derive keypair"))
        }
    }
}

/// Creates a signing keypair generated from randomness sourced from the enclave
/// runtime.
pub fn generate_signer() -> Arc<Keypair> {
    let mut randomness = [0; 32];
    switchboard_common::Gramine::read_rand(&mut randomness).unwrap();
    Arc::new(keypair_from_seed(&randomness).unwrap())
}

pub fn signer_to_pubkey(signer: Arc<Keypair>) -> std::result::Result<Pubkey, OnDemandError> {
    Ok(signer.pubkey())
}

pub fn load_keypair_fs(fs_path: &str) -> Result<Arc<Keypair>, OnDemandError> {
    match read_keypair_file(fs_path) {
        Ok(keypair) => Ok(Arc::new(keypair)),
        Err(e) => {
            if let Some(err) = e.source() {
                println!("Failed to read keypair file -- {}", err);
            }

            Err(OnDemandError::Message("Failed to read keypair file"))
        }
    }
}

/// Fetches a zero-copy account from the Solana blockchain.
///
/// # Arguments
///
/// * `client` - The Solana RPC client used to interact with the blockchain.
/// * `pubkey` - The public key of the account to fetch.
///
/// # Returns
///
/// Returns a result containing the fetched account data as the specified type `T`, or an `OnDemandError` if an error occurs.
///
/// # Errors
///
/// This function can return the following errors:
///
/// * `OnDemandError::AccountNotFound` - If the account with the specified public key is not found.
/// * `OnDemandError::Message("no discriminator found")` - If no discriminator is found in the account data.
/// * `OnDemandError::Message("Discriminator error, check the account type")` - If the discriminator in the account data does not match the expected discriminator for type `T`.
/// * `OnDemandError::Message("AnchorParseError")` - If an error occurs while parsing the account data into type `T`.
pub fn fetch_zerocopy_account<T: bytemuck::Pod + Discriminator + Owner>(
    client: &solana_client::rpc_client::RpcClient,
    pubkey: Pubkey,
) -> Result<T, OnDemandError> {
    let data = client
        .get_account_data(&pubkey)
        .map_err(|_| OnDemandError::AccountNotFound)?;

    if data.len() < T::discriminator().len() {
        return Err(OnDemandError::Message("no discriminator found"));
    }

    let mut disc_bytes = [0u8; 8];
    disc_bytes.copy_from_slice(&data[..8]);
    if disc_bytes != T::discriminator() {
        return Err(OnDemandError::CustomMessage(
            format!("Discriminator error, check the account type of {pubkey}"),
        ));
    }

    Ok(*bytemuck::try_from_bytes::<T>(&data[8..])
        .map_err(|_| OnDemandError::Message("AnchorParseError"))?)
}

/// Fetches the account data synchronously from the Solana blockchain using the provided client.
///
/// # Arguments
///
/// * `client` - The client used to interact with the Solana blockchain.
/// * `pubkey` - The public key of the account to fetch.
///
/// # Generic Parameters
///
/// * `C` - The type of the client, which must implement the `SyncClient` trait.
/// * `T` - The type of the account data, which must implement the `bytemuck::Pod`, `Discriminator`, and `Owner` traits.
///
/// # Returns
///
/// Returns a `Result` containing the fetched account data of type `T` if successful, or an `OnDemandError` if an error occurs.
pub fn fetch_zerocopy_account_sync<C: SyncClient, T: bytemuck::Pod + Discriminator + Owner>(
    client: &C,
    pubkey: Pubkey,
) -> Result<T, OnDemandError> {
    let data = client
        .get_account_data(&pubkey)
        .map_err(|_| OnDemandError::AccountNotFound)?
        .ok_or(OnDemandError::AccountNotFound)?;

    if data.len() < T::discriminator().len() {
        return Err(OnDemandError::Message("no discriminator found"));
    }

    let mut disc_bytes = [0u8; 8];
    disc_bytes.copy_from_slice(&data[..8]);
    if disc_bytes != T::discriminator() {
        return Err(OnDemandError::CustomMessage(
            format!("Discriminator error, check the account type of {pubkey}"),
        ));
    }

    Ok(*bytemuck::try_from_bytes::<T>(&data[8..])
        .map_err(|_| OnDemandError::Message("AnchorParseError"))?)
}

/// Fetches an account asynchronously using the provided client and public key.
///
/// # Arguments
///
/// * `client` - The non-blocking RPC client used to fetch the account.
/// * `pubkey` - The public key of the account to fetch.
///
/// # Generic Parameters
///
/// * `T` - The type of the account data. Must implement `bytemuck::Pod`, `Discriminator`, and `Owner`.
///
/// # Returns
///
/// Returns a `Result` containing the fetched account data of type `T` if successful, or an `OnDemandError` if an error occurs.
///
/// # Errors
///
/// This function can return the following errors:
///
/// * `OnDemandError::AccountNotFound` - If the account is not found.
/// * `OnDemandError::Message("no discriminator found")` - If no discriminator is found in the account data.
/// * `OnDemandError::Message("Discriminator error, check the account type")` - If the discriminator does not match the expected value.
/// * `OnDemandError::Message("AnchorParseError")` - If there is an error parsing the account data into type `T`.
///
/// # Example
///
/// ```rust
/// use switchboard_solana::client::NonblockingRpcClient;
/// use switchboard_solana::error::OnDemandError;
/// use switchboard_solana::types::{Discriminator, Owner};
/// use bytemuck::Pod;
/// use solana_sdk::pubkey::Pubkey;
///
/// async fn example(client: &NonblockingRpcClient, pubkey: Pubkey) -> Result<(), OnDemandError> {
///     let account_data: MyAccountType = fetch_zerocopy_account_async(client, pubkey).await?;
///     // Do something with the fetched account data...
///     Ok(())
/// }
/// ```
pub async fn fetch_zerocopy_account_async<T: bytemuck::Pod + Discriminator + Owner>(
    client: &NonblockingRpcClient,
    pubkey: Pubkey,
) -> Result<T, OnDemandError> {
    let data = client
        .get_account_data(&pubkey)
        .await
        .map_err(|_| OnDemandError::AccountNotFound)?;

    if data.len() < T::discriminator().len() {
        return Err(OnDemandError::Message("no discriminator found"));
    }

    let mut disc_bytes = [0u8; 8];
    disc_bytes.copy_from_slice(&data[..8]);
    if disc_bytes != T::discriminator() {
        let err = format!("Discriminator error, check the account type of {pubkey}");
        return Err(OnDemandError::CustomMessage(err));
    }

    Ok(*bytemuck::try_from_bytes::<T>(&data[8..])
        .map_err(|_| OnDemandError::Message("AnchorParseError"))?)
}

pub fn fetch_borsh_account<T: Discriminator + Owner + AccountDeserialize>(
    client: &solana_client::rpc_client::RpcClient,
    pubkey: Pubkey,
) -> Result<T, OnDemandError> {
    let account_data = client
        .get_account_data(&pubkey)
        .map_err(|_| OnDemandError::AccountNotFound)?;

    T::try_deserialize(&mut account_data.as_slice())
        .map_err(|_| OnDemandError::Message("AnchorParseError"))
}

pub async fn fetch_borsh_account_async<T: Discriminator + Owner + AccountDeserialize>(
    client: &NonblockingRpcClient,
    pubkey: Pubkey,
) -> Result<T, OnDemandError> {
    let account_data = client
        .get_account_data(&pubkey)
        .await
        .map_err(|_| OnDemandError::AccountNotFound)?;

    T::try_deserialize(&mut account_data.as_slice())
        .map_err(|_| OnDemandError::Message("AnchorParseError"))
}

pub fn fetch_borsh_account_sync<C: SyncClient, T: Discriminator + Owner + AccountDeserialize>(
    client: &C,
    pubkey: Pubkey,
) -> Result<T, OnDemandError> {
    let data = client
        .get_account_data(&pubkey)
        .map_err(|_| OnDemandError::AccountNotFound)?
        .ok_or(OnDemandError::AccountNotFound)?;

    T::try_deserialize(&mut data.as_slice()).map_err(|_| OnDemandError::Message("AnchorParseError"))
}

pub async fn subscribe<E, F, T>(
    program_id: Pubkey,
    url: &str,
    client: Arc<RwLock<AnchorClient>>,
    quote_key: Arc<Pubkey>,
    enclave_key: Arc<RwLock<Keypair>>,
    payer: Arc<Keypair>,
    async_fn: F,
) where
    F: Fn(Arc<RwLock<AnchorClient>>, Arc<Pubkey>, Arc<RwLock<Keypair>>, Arc<Keypair>, E) -> T
        + Send
        + Sync
        + 'static,
    T: Future<Output = ()> + Send + 'static,
    E: Event,
{
    // TODO: This may pull events from other programs if targeted but the
    // request still goes through verification so not a fatal issue.
    let pubsub_client = PubsubClient::new(url).await.unwrap();
    loop {
        let (mut r, _handler) = pubsub_client
            .logs_subscribe(
                RpcTransactionLogsFilter::Mentions(vec![program_id.to_string()]),
                RpcTransactionLogsConfig {
                    commitment: Some(CommitmentConfig::processed()),
                },
            )
            .await
            .unwrap();
        while let Some(event) = r.next().await {
            let log: String = event.value.logs.join(" ");
            for w in log.split(' ') {
                let decoded = general_purpose::STANDARD.decode(w);
                if decoded.is_err() {
                    continue;
                }
                let decoded = decoded.unwrap();
                if decoded.len() < 8 {
                    continue;
                }
                if decoded[..8] != E::DISCRIMINATOR {
                    continue;
                }

                if let Ok(event) = E::try_from_slice(&decoded[8..]) {
                    async_fn(
                        client.clone(),
                        quote_key.clone(),
                        enclave_key.clone(),
                        payer.clone(),
                        event,
                    )
                    .await;
                }
            }
        }
    }
}

pub async fn subscribe_ro<E, F, T>(program_id: Pubkey, url: &str, f: F)
where
    F: Fn(E) -> T + Send + Sync + 'static,
    T: Future<Output = ()> + Send + 'static,
    E: Event,
{
    // TODO: This may pull events from other programs if targeted but the
    // request still goes through verification so not a fatal issue.
    loop {
        let pubsub_client = PubsubClient::new(url).await.unwrap();
        let res = pubsub_client
            .logs_subscribe(
                RpcTransactionLogsFilter::Mentions(vec![program_id.to_string()]),
                RpcTransactionLogsConfig {
                    commitment: Some(CommitmentConfig::processed()),
                },
            )
            .await;
        if res.is_err() {
            println!("ERROR Subscription failure");
            continue;
        }
        let (mut r, _handler) = res.unwrap();
        // let mut ctxs: Vec<String> = vec![];
        while let Some(event) = r.next().await {
            for line in event.value.logs {
                // if let Some(pid) = extract_program_enter(&line) {
                // ctxs.push(Pubkey::from_str(&pid).unwrap());
                // }
                // if let Some(_pid) = extract_program_exit(&line) {
                // ctxs.pop();
                // }
                // if ctxs.last() != Some(&program_id) {
                // continue;
                // }
                for w in line.split(' ') {
                    let decoded = general_purpose::STANDARD.decode(w);
                    if decoded.is_err() {
                        continue;
                    }
                    let decoded = decoded.unwrap();
                    if decoded.len() < 8 {
                        continue;
                    }
                    if decoded[..8] != E::DISCRIMINATOR {
                        continue;
                    }
                    println!("DISCRIMINATOR_MATCH");
                    let event = E::try_from_slice(&decoded[8..]);
                    if event.is_ok() {
                        println!("EVENT_PARSE_SUCCESS");
                        f(event.unwrap()).await;
                    } else {
                        println!("EVENT_PARSE_FAILURE");
                    }
                }
            }
        }
    }
}

type GenericError = Box<dyn std::error::Error + Send + Sync>;

pub async fn subscribe_v2<E, F, EF, T>(subcription_url: &str, f: F, on_error: EF) -> Result<(), GenericError>
where
    F: Fn(E) -> T + Copy + Send + Sync + 'static,
    EF: Fn(GenericError) + Copy + Send + Sync + 'static,
    T: Future<Output = Result<(), GenericError>> + Send,
    E: Event + Send + Sync + Clone + Debug + 'static,
{
    let program_id = SWITCHBOARD_PROGRAM_ID;
    let discriminator: String = general_purpose::STANDARD
        .encode(E::DISCRIMINATOR)
        .replace("=", "");
    let prefix = "Program data: ".to_owned() + discriminator.as_str();
    // TODO: This may pull events from other programs if targeted but the
    // request still goes through verification so not a fatal issue.
    loop {
        let pubsub_client = PubsubClient::new(subcription_url).await.unwrap();
        let res = pubsub_client
            .logs_subscribe(
                RpcTransactionLogsFilter::Mentions(vec![program_id.to_string()]),
                RpcTransactionLogsConfig {
                    commitment: Some(CommitmentConfig::processed()),
                },
            )
            .await;
        if res.is_err() {
            println!("Subscription Failure");
            continue;
        }
        let (mut r, _handler) = res.unwrap();
        while let Some(event) = r.next().await {
            let filtered = event
                .value
                .logs
                .into_iter()
                .filter(|s| s.starts_with(&prefix))
                .filter_map(|s| general_purpose::STANDARD.decode(&s[14..]).ok())
                .map(|b| b.to_vec());
            for decoded in filtered {
                if let Ok(event) = E::try_from_slice(&decoded[8..]) {
                    tokio::spawn(async move {
                        if let Err(err) = f(event.clone()).await {
                            on_error(err);
                            // emit_metric!(UNCAUGHT_ERROR_COUNTER).inc();
                            // error!("UncaughtError", {
                                // error: err.to_string(),
                                // event: format!("{:?}", event),
                            // });
                        }
                    });
                }
            }
        }
    }
}
