//! Transaction-related API operations.

use std::{str::FromStr, time::Duration};

use alloy_primitives::B256;
use om_primitives_types::transaction::{envelope::RawTransactionEnvelope, payload::PaymentPayload};
use om_rest_types::{
    FinalizedTransaction, Transaction,
    requests::{FeeEstimateRequest, PaymentTransactionRequest},
    responses::{FeeEstimate, TransactionReceipt, TransactionResponse},
};
use tokio::time::{Instant, sleep};

use crate::{
    client::{
        Client,
        config::{
            API_VERSION, api_path,
            endpoints::transactions::{BY_HASH, ESTIMATE_FEE, FINALIZED_BY_HASH, PAYMENT, RAW, RECEIPT_BY_HASH},
        },
    },
    crypto::sign_transaction_payload,
    error::{Error, Result},
    utils::{signature_hash_for_counter_sign, verify_bls_aggregate_signature},
};

const DEFAULT_RECEIPT_TIMEOUT: Duration = Duration::from_secs(30);
const DEFAULT_RECEIPT_POLL_INTERVAL: Duration = Duration::from_millis(50);

impl Client {
    /// Send a payment transaction.
    ///
    /// # Arguments
    ///
    /// * `payload` - Payment transaction parameters
    /// * `private_key` - Private key for signing the transaction
    ///
    /// # Returns
    ///
    /// The payment response containing the transaction hash.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use std::str::FromStr;
    ///
    /// use alloy_primitives::{Address, U256};
    /// use onemoney_protocol::{Client, NamedChain, PaymentPayload};
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = Client::mainnet()?;
    ///
    ///     let payload = PaymentPayload {
    ///         chain_id: NamedChain::TESTNET_CHAIN_ID,
    ///         nonce: 0,
    ///         recipient: Address::from_str("0x742d35Cc6634C0532925a3b8D91D6F4A81B8Cbc0")?,
    ///         value: U256::from(1000000000000000000u64), // 1 token
    ///         token: Address::from_str("0x1234567890abcdef1234567890abcdef12345678")?,
    ///     };
    ///
    ///     let private_key = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
    ///     let result = client.send_payment(payload, private_key).await?;
    ///     println!("Transaction hash: {}", result.hash);
    ///
    ///     Ok(())
    /// }
    /// ```
    pub async fn send_payment(&self, data: PaymentPayload, private_key: &str) -> Result<TransactionResponse> {
        let signature = sign_transaction_payload(&data, private_key)?;
        let request = PaymentTransactionRequest { data, signature };

        let path = api_path(PAYMENT);
        self.post(&path, &request).await
    }

    /// Submit a raw transaction envelope.
    ///
    /// This is used for advanced transaction types such as multi-sig creation
    /// and multi-sig payments.
    pub async fn submit_raw_transaction(&self, envelope: RawTransactionEnvelope) -> Result<TransactionResponse> {
        let path = api_path(RAW);
        self.post(&path, &envelope).await
    }

    /// Get transaction by hash.
    ///
    /// # Arguments
    ///
    /// * `hash` - Transaction hash
    ///
    /// # Returns
    ///
    /// The transaction details.
    pub async fn get_transaction_by_hash(&self, hash: &str) -> Result<Transaction> {
        let path = format!("{}{}?hash={}", API_VERSION, BY_HASH, hash);
        self.get(&path).await
    }

    /// Get transaction receipt by hash.
    ///
    /// # Arguments
    ///
    /// * `hash` - Transaction hash
    ///
    /// # Returns
    ///
    /// The transaction receipt.
    pub async fn get_transaction_receipt_by_hash(&self, hash: &str) -> Result<TransactionReceipt> {
        let path = format!("{}{}?hash={}", API_VERSION, RECEIPT_BY_HASH, hash);
        self.get(&path).await
    }

    /// Wait for a transaction receipt using the default timeout.
    ///
    /// This method polls the receipt endpoint every 50ms for up to 30 seconds.
    pub async fn wait_for_transaction_receipt(&self, hash: &str) -> Result<TransactionReceipt> {
        self.wait_for_transaction_receipt_with_timeout(hash, DEFAULT_RECEIPT_TIMEOUT)
            .await
    }

    /// Wait for a transaction receipt with a custom timeout.
    ///
    /// # Arguments
    /// * `hash` - Transaction hash
    /// * `timeout` - Maximum duration to poll before returning a timeout error
    pub async fn wait_for_transaction_receipt_with_timeout(
        &self,
        hash: &str,
        timeout: Duration,
    ) -> Result<TransactionReceipt> {
        let hash_owned = hash.to_string();
        let request_path = format!("{}{}?hash={}", API_VERSION, RECEIPT_BY_HASH, hash);

        poll_for_transaction_receipt(
            || async { self.get_transaction_receipt_by_hash(&hash_owned).await },
            request_path,
            timeout,
            DEFAULT_RECEIPT_POLL_INTERVAL,
        )
        .await
    }

    /// Estimate transaction fee.
    ///
    /// # Arguments
    ///
    /// * `request` - Fee estimation parameters
    ///
    /// # Returns
    ///
    /// The estimated fee.
    pub async fn estimate_fee(&self, request: FeeEstimateRequest) -> Result<FeeEstimate> {
        let path = api_path(ESTIMATE_FEE);
        let full_path = format!(
            "{}?from={}&to={}&token={}&value={}",
            path, request.from, request.to, request.token, request.value,
        );
        self.get(&full_path).await
    }

    /// Get finalized transaction and receipt by hash.
    ///
    /// # Arguments
    ///
    /// * `hash` - Transaction hash
    ///
    /// # Returns
    ///
    /// The finalized transaction and receipt.
    pub async fn get_finalized_transaction_by_hash(&self, hash: &str) -> Result<FinalizedTransaction> {
        let path = format!("{}{}?hash={}", API_VERSION, FINALIZED_BY_HASH, hash);
        self.get(&path).await
    }

    /// Get and verify finalized transaction by hash with BLS signature
    /// verification.
    ///
    /// This is a convenience method that fetches the finalized transaction and
    /// automatically verifies the BLS aggregate signature from validators.
    ///
    /// # Arguments
    ///
    /// * `hash` - Transaction hash (with or without 0x prefix)
    ///
    /// # Returns
    ///
    /// The finalized transaction if signature verification passes, error
    /// otherwise.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use onemoney_protocol::Client;
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = Client::testnet()?;
    ///
    ///     // This will fetch AND verify the transaction
    ///     let finalized_tx = client
    ///         .get_and_verify_finalized_transaction_by_hash("0x1234...")
    ///         .await?;
    ///
    ///     println!("Transaction verified! Epoch: {}", finalized_tx.epoch);
    ///     Ok(())
    /// }
    /// ```
    pub async fn get_and_verify_finalized_transaction_by_hash(&self, hash: &str) -> Result<FinalizedTransaction> {
        // Fetch the finalized transaction
        let finalized_tx = self.get_finalized_transaction_by_hash(hash).await?;

        // Parse transaction hash
        let tx_hash =
            B256::from_str(hash).map_err(|e| Error::validation("hash", format!("Invalid transaction hash: {}", e)))?;

        // Compute the counter-sign message hash
        let message_hash = signature_hash_for_counter_sign(&tx_hash, &finalized_tx.epoch);

        // Verify the BLS signature
        verify_bls_aggregate_signature(&message_hash, &finalized_tx.counter_signature)?;

        Ok(finalized_tx)
    }
}

async fn poll_for_transaction_receipt<F, Fut>(
    mut fetch_receipt: F,
    request_path: String,
    timeout: Duration,
    poll_interval: Duration,
) -> Result<TransactionReceipt>
where
    F: FnMut() -> Fut,
    Fut: Future<Output = Result<TransactionReceipt>>,
{
    if timeout.is_zero() {
        return Err(Error::invalid_parameter("timeout", "Timeout must be greater than zero"));
    }
    if poll_interval.is_zero() {
        return Err(Error::invalid_parameter(
            "poll_interval",
            "Poll interval must be greater than zero",
        ));
    }

    let start = Instant::now();

    loop {
        match fetch_receipt().await {
            Ok(receipt) => return Ok(receipt),
            Err(err) => {
                if !matches!(err, Error::ResourceNotFound { .. }) {
                    return Err(err);
                }
            }
        }

        let elapsed = start.elapsed();
        if elapsed >= timeout {
            return Err(Error::request_timeout(
                request_path.clone(),
                duration_to_millis(timeout),
            ));
        }

        if let Some(remaining) = timeout.checked_sub(elapsed) {
            let sleep_duration = poll_interval.min(remaining);
            sleep(sleep_duration).await;
        } else {
            return Err(Error::request_timeout(
                request_path.clone(),
                duration_to_millis(timeout),
            ));
        }
    }
}

fn duration_to_millis(duration: Duration) -> u64 {
    duration.as_millis().min(u128::from(u64::MAX)) as u64
}

#[cfg(test)]
mod tests {
    use std::{collections::VecDeque, str::FromStr, sync::Mutex, time::Duration};

    use alloy_primitives::{Address, B256, U256};

    use super::*;
    use crate::NamedChain;

    #[test]
    fn test_payment_payload_alloy_rlp() {
        use alloy_rlp::Encodable as AlloyEncodable;

        let payload = PaymentPayload {
            chain_id: NamedChain::TESTNET_CHAIN_ID,
            nonce: 0,
            recipient: Address::from_str("0x742d35Cc6634C0532925a3b8D91D6F4A81B8Cbc0")
                .expect("Test data should be valid"),
            value: U256::from(1000000000000000000u64),
            token: Address::from_str("0x1234567890abcdef1234567890abcdef12345678").expect("Test data should be valid"),
        };

        let mut encoded = Vec::new();
        payload.encode(&mut encoded);
        assert!(!encoded.is_empty());
    }

    #[test]
    fn test_fee_estimate_request() {
        let request = FeeEstimateRequest {
            from: "0x742d35Cc6634C0532925a3b8D91D6F4A81B8Cbc0".to_string(),
            to: "0x742d35Cc6634C0532925a3b8D91D6F4A81B8Cbc1".to_string(),
            token: "0x1234567890abcdef1234567890abcdef12345678".to_string(),
            value: "1000000000000000000".to_string(),
        };

        // Test serialization
        let json = serde_json::to_string(&request).expect("Should serialize");
        assert!(json.contains("0x742d35Cc6634C0532925a3b8D91D6F4A81B8Cbc0"));
        assert!(json.contains("0x742d35Cc6634C0532925a3b8D91D6F4A81B8Cbc1"));
        assert!(json.contains("0x1234567890abcdef1234567890abcdef12345678"));
        assert!(json.contains("1000000000000000000"));
    }

    #[test]
    fn test_finalized_transaction_api_path_construction() {
        let hash = "0x902006665c369834a0cf52eea2780f934a90b3c86a3918fb57371ac1fbbd7777";
        let expected_path = format!("{}{}?hash={}", API_VERSION, FINALIZED_BY_HASH, hash);

        assert!(expected_path.contains("/v1"));
        assert!(expected_path.contains("/transactions/finalized/by_hash"));
        assert!(expected_path.contains("hash=0x902006665c369834a0cf52eea2780f934a90b3c86a3918fb57371ac1fbbd7777"));
    }

    #[test]
    fn test_finalized_transaction_structure() {
        use alloy_primitives::{Address, B256};
        use om_rest_types::{FinalizedTransaction, RestBlsAggregateSignature, responses::TransactionReceipt};

        let finalized_tx = FinalizedTransaction {
            epoch: 100,
            receipt: TransactionReceipt {
                success: true,
                transaction_hash: B256::from_str("0x902006665c369834a0cf52eea2780f934a90b3c86a3918fb57371ac1fbbd7777")
                    .expect("Test data should be valid"),
                transaction_index: Some(5),
                checkpoint_hash: Some(
                    B256::from_str("0x20e081da293ae3b81e30f864f38f6911663d7f2cf98337fca38db3cf5bbe7a8f")
                        .expect("Test data should be valid"),
                ),
                checkpoint_number: Some(1500),
                fee_used: 1000000,
                from: Address::from_str("0x742d35Cc6634C0532925a3b8D91D6F4A81B8Cbc0")
                    .expect("Test data should be valid"),
                recipient: Some(
                    Address::from_str("0x1234567890abcdef1234567890abcdef12345678").expect("Test data should be valid"),
                ),
                token_address: None,
                success_info: None,
            },
            counter_signature: RestBlsAggregateSignature::default(),
        };

        assert_eq!(finalized_tx.epoch, 100);
        assert!(finalized_tx.receipt.success);
        assert_eq!(finalized_tx.receipt.fee_used, 1000000);
    }

    #[test]
    fn test_finalized_transaction_json_output() {
        use alloy_primitives::{Address, B256};
        use om_rest_types::{FinalizedTransaction, RestBlsAggregateSignature, responses::TransactionReceipt};

        let finalized_tx = FinalizedTransaction {
            epoch: 200,
            receipt: TransactionReceipt {
                success: true,
                transaction_hash: B256::from_str("0x902006665c369834a0cf52eea2780f934a90b3c86a3918fb57371ac1fbbd7777")
                    .expect("Test data should be valid"),
                transaction_index: Some(0),
                checkpoint_hash: Some(
                    B256::from_str("0x20e081da293ae3b81e30f864f38f6911663d7f2cf98337fca38db3cf5bbe7a8f")
                        .expect("Test data should be valid"),
                ),
                checkpoint_number: Some(1500),
                fee_used: 1000000,
                from: Address::from_str("0x742d35Cc6634C0532925a3b8D91D6F4A81B8Cbc0")
                    .expect("Test data should be valid"),
                recipient: Some(
                    Address::from_str("0x1234567890abcdef1234567890abcdef12345678").expect("Test data should be valid"),
                ),
                token_address: Some(
                    Address::from_str("0xabcdef1234567890abcdef1234567890abcdef12").expect("Test data should be valid"),
                ),
                success_info: None,
            },
            counter_signature: RestBlsAggregateSignature::new(
                "0xff".to_string(),
                "0x1234".to_string(),
                vec!["0xpubkey1".to_string()],
            ),
        };

        let json = serde_json::to_string(&finalized_tx).expect("Should serialize to JSON");

        assert!(json.contains("\"epoch\":200"));
        assert!(
            json.contains(
                "\"transaction_hash\":\"0x902006665c369834a0cf52eea2780f934a90b3c86a3918fb57371ac1fbbd7777\""
            )
        );
        assert!(json.contains("\"success\":true"));
        assert!(json.contains("\"fee_used\":\"1000000\""));
        assert!(json.contains("\"counter_signature\""));
    }

    fn sample_receipt(hash: &str) -> TransactionReceipt {
        TransactionReceipt {
            success: true,
            transaction_hash: B256::from_str(hash).expect("valid hash"),
            transaction_index: Some(0),
            checkpoint_hash: None,
            checkpoint_number: Some(42),
            fee_used: 1,
            from: Address::from_str("0x0000000000000000000000000000000000000001").expect("valid address"),
            recipient: Some(Address::from_str("0x0000000000000000000000000000000000000002").expect("valid address")),
            token_address: Some(
                Address::from_str("0x0000000000000000000000000000000000000003").expect("valid address"),
            ),
            success_info: None,
        }
    }

    #[tokio::test]
    async fn test_wait_for_transaction_receipt_eventually_succeeds() {
        let tx_hash = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
        let request_path = format!("/v1/transactions/receipt/by_hash?hash={tx_hash}");

        let responses = Mutex::new(VecDeque::from([
            Err(Error::resource_not_found("receipt", "pending")),
            Ok(sample_receipt(tx_hash)),
        ]));

        let receipt = poll_for_transaction_receipt(
            || {
                let result = responses
                    .lock()
                    .expect("lock poisoned")
                    .pop_front()
                    .expect("response available");
                async move { result }
            },
            request_path,
            Duration::from_millis(100),
            Duration::from_millis(10),
        )
        .await
        .expect("should eventually succeed");

        assert!(receipt.success);
        assert_eq!(receipt.checkpoint_number, Some(42));
        assert_eq!(
            receipt.recipient,
            Some(Address::from_str("0x0000000000000000000000000000000000000002").unwrap())
        );
    }

    #[tokio::test]
    async fn test_wait_for_transaction_receipt_respects_errors() {
        let request_path = "/v1/transactions/receipt/by_hash?hash=0xbb".to_string();
        let responses = Mutex::new(VecDeque::from([Err(Error::http_transport("boom", Some(500)))]));

        let err = poll_for_transaction_receipt(
            || {
                let result = responses
                    .lock()
                    .expect("lock poisoned")
                    .pop_front()
                    .expect("response available");
                async move { result }
            },
            request_path,
            Duration::from_millis(50),
            Duration::from_millis(10),
        )
        .await
        .expect_err("should propagate error");

        assert!(matches!(err, Error::HttpTransport { .. }));
    }

    #[tokio::test]
    async fn test_wait_for_transaction_receipt_with_zero_timeout_is_rejected() {
        let err = poll_for_transaction_receipt(
            || async {
                Ok(sample_receipt(
                    "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
                ))
            },
            "/v1/transactions/receipt/by_hash?hash=0xcc".to_string(),
            Duration::from_secs(0),
            Duration::from_millis(10),
        )
        .await
        .expect_err("zero timeout invalid");

        assert!(matches!(err, Error::InvalidParameter { .. }));
    }
}
