use alloy::network::{Ethereum, EthereumWallet, Network, TransactionBuilder};
use alloy::primitives::{Address, Bytes, U256};
use alloy::providers::{Provider, ProviderBuilder};
use alloy::signers::local::PrivateKeySigner;
use roboticus_core::{Result, RoboticusError};

use crate::wallet::Wallet;

#[derive(Debug, Clone)]
pub struct EvmContractCall {
    pub to: String,
    pub data_hex: String,
    pub value_wei: Option<String>,
    pub gas_limit: Option<u64>,
    pub max_fee_per_gas_wei: Option<String>,
    pub max_priority_fee_per_gas_wei: Option<String>,
}

pub async fn submit_evm_contract_call(wallet: &Wallet, call: &EvmContractCall) -> Result<String> {
    let to = parse_address(&call.to)?;
    let input = parse_calldata(&call.data_hex)?;
    let value = parse_optional_u256(call.value_wei.as_deref())?;
    let max_fee = parse_optional_u128(call.max_fee_per_gas_wei.as_deref())?;
    let max_priority_fee = parse_optional_u128(call.max_priority_fee_per_gas_wei.as_deref())?;

    let key_bytes: &[u8; 32] = wallet
        .private_key_bytes()
        .try_into()
        .map_err(|_| RoboticusError::Wallet("invalid private key length".into()))?;
    let signer = PrivateKeySigner::from_bytes(key_bytes.into())
        .map_err(|e| RoboticusError::Wallet(format!("invalid private key: {e}")))?;
    let provider = ProviderBuilder::new()
        .with_recommended_fillers()
        .wallet(EthereumWallet::from(signer))
        .on_http(
            wallet
                .rpc_url()
                .parse()
                .map_err(|e| RoboticusError::Wallet(format!("invalid RPC URL: {e}")))?,
        );

    let mut tx = <Ethereum as Network>::TransactionRequest::default()
        .with_to(to)
        .with_input(input)
        .with_chain_id(wallet.chain_id());
    if let Some(value) = value {
        tx = tx.with_value(value);
    }
    if let Some(gas_limit) = call.gas_limit {
        tx = tx.with_gas_limit(gas_limit);
    }
    if let Some(max_fee) = max_fee {
        tx = tx.with_max_fee_per_gas(max_fee);
    }
    if let Some(max_priority_fee) = max_priority_fee {
        tx = tx.with_max_priority_fee_per_gas(max_priority_fee);
    }

    let pending = provider
        .send_transaction(tx)
        .await
        .map_err(|e| RoboticusError::Wallet(format!("contract call submission failed: {e}")))?;
    Ok(format!("{:#x}", pending.tx_hash()))
}

pub async fn get_evm_transaction_receipt_status(
    wallet: &Wallet,
    tx_hash: &str,
) -> Result<Option<bool>> {
    let tx_hash = tx_hash.trim();
    if tx_hash.is_empty() {
        return Err(RoboticusError::Wallet("tx_hash must be non-empty".into()));
    }
    let rpc_body = serde_json::json!({
        "jsonrpc": "2.0",
        "method": "eth_getTransactionReceipt",
        "params": [tx_hash],
        "id": 1,
    });
    let resp = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(30))
        .build()
        .map_err(|e| RoboticusError::Wallet(format!("HTTP client build failed: {e}")))?
        .post(wallet.rpc_url())
        .json(&rpc_body)
        .send()
        .await
        .map_err(|e| RoboticusError::Wallet(format!("RPC request failed: {e}")))?;
    let body: serde_json::Value = resp
        .json()
        .await
        .map_err(|e| RoboticusError::Wallet(format!("RPC response parse failed: {e}")))?;
    if let Some(err) = body.get("error") {
        return Err(RoboticusError::Wallet(format!("RPC error: {err}")));
    }
    parse_receipt_status_response(&body)
}

fn parse_address(s: &str) -> Result<Address> {
    s.parse::<Address>()
        .map_err(|e| RoboticusError::Wallet(format!("invalid destination address: {e}")))
}

fn parse_calldata(s: &str) -> Result<Bytes> {
    let trimmed = s.trim();
    if trimmed.is_empty() {
        return Err(RoboticusError::Wallet("calldata must be non-empty".into()));
    }
    let hex_body = trimmed.strip_prefix("0x").unwrap_or(trimmed);
    let bytes = hex::decode(hex_body)
        .map_err(|e| RoboticusError::Wallet(format!("invalid calldata hex: {e}")))?;
    Ok(Bytes::from(bytes))
}

fn parse_optional_u256(value: Option<&str>) -> Result<Option<U256>> {
    let Some(value) = value.map(str::trim).filter(|s| !s.is_empty()) else {
        return Ok(None);
    };
    parse_u256(value).map(Some)
}

fn parse_optional_u128(value: Option<&str>) -> Result<Option<u128>> {
    let Some(value) = value.map(str::trim).filter(|s| !s.is_empty()) else {
        return Ok(None);
    };
    parse_u128(value).map(Some)
}

fn parse_u256(value: &str) -> Result<U256> {
    if let Some(hex_value) = value.strip_prefix("0x") {
        U256::from_str_radix(hex_value, 16)
            .map_err(|e| RoboticusError::Wallet(format!("invalid hex quantity '{value}': {e}")))
    } else {
        U256::from_str_radix(value, 10)
            .map_err(|e| RoboticusError::Wallet(format!("invalid decimal quantity '{value}': {e}")))
    }
}

fn parse_u128(value: &str) -> Result<u128> {
    if let Some(hex_value) = value.strip_prefix("0x") {
        u128::from_str_radix(hex_value, 16)
            .map_err(|e| RoboticusError::Wallet(format!("invalid hex quantity '{value}': {e}")))
    } else {
        value
            .parse::<u128>()
            .map_err(|e| RoboticusError::Wallet(format!("invalid decimal quantity '{value}': {e}")))
    }
}

fn parse_receipt_status_response(body: &serde_json::Value) -> Result<Option<bool>> {
    let Some(result) = body.get("result") else {
        return Err(RoboticusError::Wallet(
            "missing result in transaction receipt response".into(),
        ));
    };
    if result.is_null() {
        return Ok(None);
    }
    let status = result
        .get("status")
        .and_then(|v| v.as_str())
        .ok_or_else(|| RoboticusError::Wallet("transaction receipt missing status field".into()))?;
    match status {
        "0x1" | "0x01" | "1" => Ok(Some(true)),
        "0x0" | "0x00" | "0" => Ok(Some(false)),
        other => Err(RoboticusError::Wallet(format!(
            "unexpected transaction receipt status '{other}'"
        ))),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use roboticus_core::config::WalletConfig;
    use tempfile::TempDir;
    use wiremock::matchers::{body_json, method};
    use wiremock::{Mock, MockServer, ResponseTemplate};

    async fn wallet_with_rpc(rpc_url: &str) -> Wallet {
        let dir = TempDir::new().unwrap();
        let config = WalletConfig {
            path: dir.path().join("wallet.json"),
            chain_id: 8453,
            rpc_url: rpc_url.to_string(),
        };
        Wallet::load_or_generate(&config).await.unwrap()
    }

    #[test]
    fn parse_calldata_rejects_empty_string() {
        let err = parse_calldata("").unwrap_err();
        assert!(err.to_string().contains("calldata must be non-empty"));
    }

    #[test]
    fn parse_calldata_rejects_invalid_hex() {
        let err = parse_calldata("0xnothex").unwrap_err();
        assert!(err.to_string().contains("invalid calldata hex"));
    }

    #[test]
    fn parse_u256_accepts_hex_and_decimal() {
        assert_eq!(parse_u256("16").unwrap(), U256::from(16));
        assert_eq!(parse_u256("0x10").unwrap(), U256::from(16));
    }

    #[test]
    fn parse_u128_accepts_hex_and_decimal() {
        assert_eq!(parse_u128("16").unwrap(), 16);
        assert_eq!(parse_u128("0x10").unwrap(), 16);
    }

    #[test]
    fn parse_address_rejects_invalid_values() {
        let err = parse_address("not-an-address").unwrap_err();
        assert!(err.to_string().contains("invalid destination address"));
    }

    #[test]
    fn parse_calldata_accepts_prefixed_and_unprefixed_hex() {
        assert_eq!(
            parse_calldata("0xdeadbeef").unwrap(),
            Bytes::from(hex::decode("deadbeef").unwrap())
        );
        assert_eq!(
            parse_calldata("deadbeef").unwrap(),
            Bytes::from(hex::decode("deadbeef").unwrap())
        );
    }

    #[test]
    fn parse_optional_quantities_treat_none_and_empty_as_absent() {
        assert_eq!(parse_optional_u256(None).unwrap(), None);
        assert_eq!(parse_optional_u256(Some("   ")).unwrap(), None);
        assert_eq!(parse_optional_u128(None).unwrap(), None);
        assert_eq!(parse_optional_u128(Some("")).unwrap(), None);
        assert_eq!(
            parse_optional_u256(Some("0x10")).unwrap(),
            Some(U256::from(16))
        );
        assert_eq!(parse_optional_u128(Some("0x10")).unwrap(), Some(16));
    }

    #[test]
    fn parse_optional_quantities_trim_decimal_inputs() {
        assert_eq!(
            parse_optional_u256(Some(" 42 ")).unwrap(),
            Some(U256::from(42))
        );
        assert_eq!(parse_optional_u128(Some(" 42 ")).unwrap(), Some(42));
    }

    #[test]
    fn parse_u256_and_u128_reject_invalid_values() {
        assert!(parse_u256("xyz").is_err());
        assert!(parse_u256("0xnope").is_err());
        assert!(parse_u128("xyz").is_err());
        assert!(parse_u128("0xnope").is_err());
    }

    #[test]
    fn parse_receipt_status_response_handles_pending_success_and_failure() {
        assert_eq!(
            parse_receipt_status_response(&serde_json::json!({"result": null})).unwrap(),
            None
        );
        assert_eq!(
            parse_receipt_status_response(&serde_json::json!({"result": {"status":"0x1"}}))
                .unwrap(),
            Some(true)
        );
        assert_eq!(
            parse_receipt_status_response(&serde_json::json!({"result": {"status":"0x0"}}))
                .unwrap(),
            Some(false)
        );
        assert_eq!(
            parse_receipt_status_response(&serde_json::json!({"result": {"status":"0x01"}}))
                .unwrap(),
            Some(true)
        );
        assert_eq!(
            parse_receipt_status_response(&serde_json::json!({"result": {"status":"1"}})).unwrap(),
            Some(true)
        );
        assert_eq!(
            parse_receipt_status_response(&serde_json::json!({"result": {"status":"0x00"}}))
                .unwrap(),
            Some(false)
        );
        assert_eq!(
            parse_receipt_status_response(&serde_json::json!({"result": {"status":"0"}})).unwrap(),
            Some(false)
        );
    }

    #[test]
    fn parse_receipt_status_response_errors_for_missing_fields_and_unknown_status() {
        assert!(parse_receipt_status_response(&serde_json::json!({})).is_err());
        assert!(parse_receipt_status_response(&serde_json::json!({"result": {}})).is_err());
        let err = parse_receipt_status_response(&serde_json::json!({"result": {"status":"0x2"}}))
            .unwrap_err();
        assert!(
            err.to_string()
                .contains("unexpected transaction receipt status")
        );
    }

    #[tokio::test]
    async fn submit_evm_contract_call_rejects_invalid_inputs_before_network() {
        let wallet = Wallet::test_mock();
        let invalid_to = EvmContractCall {
            to: "not-an-address".into(),
            data_hex: "0xdeadbeef".into(),
            value_wei: None,
            gas_limit: None,
            max_fee_per_gas_wei: None,
            max_priority_fee_per_gas_wei: None,
        };
        assert!(
            submit_evm_contract_call(&wallet, &invalid_to)
                .await
                .unwrap_err()
                .to_string()
                .contains("invalid destination address")
        );

        let invalid_data = EvmContractCall {
            to: "0x0000000000000000000000000000000000000001".into(),
            data_hex: "0xnothex".into(),
            value_wei: None,
            gas_limit: None,
            max_fee_per_gas_wei: None,
            max_priority_fee_per_gas_wei: None,
        };
        assert!(
            submit_evm_contract_call(&wallet, &invalid_data)
                .await
                .unwrap_err()
                .to_string()
                .contains("invalid calldata hex")
        );

        let invalid_value = EvmContractCall {
            to: "0x0000000000000000000000000000000000000001".into(),
            data_hex: "0xdeadbeef".into(),
            value_wei: Some("bogus".into()),
            gas_limit: None,
            max_fee_per_gas_wei: None,
            max_priority_fee_per_gas_wei: None,
        };
        assert!(
            submit_evm_contract_call(&wallet, &invalid_value)
                .await
                .unwrap_err()
                .to_string()
                .contains("invalid decimal quantity")
        );
    }

    #[tokio::test]
    async fn submit_evm_contract_call_rejects_invalid_fee_fields_before_network() {
        let wallet = Wallet::test_mock();
        let invalid_max_fee = EvmContractCall {
            to: "0x0000000000000000000000000000000000000001".into(),
            data_hex: "0xdeadbeef".into(),
            value_wei: None,
            gas_limit: Some(21_000),
            max_fee_per_gas_wei: Some("bad".into()),
            max_priority_fee_per_gas_wei: None,
        };
        assert!(
            submit_evm_contract_call(&wallet, &invalid_max_fee)
                .await
                .unwrap_err()
                .to_string()
                .contains("invalid decimal quantity")
        );

        let invalid_priority_fee = EvmContractCall {
            to: "0x0000000000000000000000000000000000000001".into(),
            data_hex: "0xdeadbeef".into(),
            value_wei: None,
            gas_limit: Some(21_000),
            max_fee_per_gas_wei: None,
            max_priority_fee_per_gas_wei: Some("0xnope".into()),
        };
        assert!(
            submit_evm_contract_call(&wallet, &invalid_priority_fee)
                .await
                .unwrap_err()
                .to_string()
                .contains("invalid hex quantity")
        );
    }

    #[tokio::test]
    async fn get_evm_transaction_receipt_status_rejects_empty_hash_before_network() {
        let wallet = Wallet::test_mock();
        let err = get_evm_transaction_receipt_status(&wallet, "   ")
            .await
            .unwrap_err();
        assert!(err.to_string().contains("tx_hash must be non-empty"));
    }

    #[tokio::test]
    async fn get_evm_transaction_receipt_status_returns_pending_success_and_failure() {
        let server = MockServer::start().await;
        let wallet = wallet_with_rpc(&server.uri()).await;

        Mock::given(method("POST"))
            .and(body_json(serde_json::json!({
                "jsonrpc": "2.0",
                "method": "eth_getTransactionReceipt",
                "params": ["0xabc"],
                "id": 1,
            })))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "result": null
            })))
            .up_to_n_times(1)
            .mount(&server)
            .await;
        assert_eq!(
            get_evm_transaction_receipt_status(&wallet, "0xabc")
                .await
                .unwrap(),
            None
        );

        Mock::given(method("POST"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "result": { "status": "0x1" }
            })))
            .up_to_n_times(1)
            .mount(&server)
            .await;
        assert_eq!(
            get_evm_transaction_receipt_status(&wallet, "0xabc")
                .await
                .unwrap(),
            Some(true)
        );

        Mock::given(method("POST"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "result": { "status": "0x0" }
            })))
            .up_to_n_times(1)
            .mount(&server)
            .await;
        assert_eq!(
            get_evm_transaction_receipt_status(&wallet, "0xabc")
                .await
                .unwrap(),
            Some(false)
        );
    }

    #[tokio::test]
    async fn get_evm_transaction_receipt_status_surfaces_rpc_error() {
        let server = MockServer::start().await;
        let wallet = wallet_with_rpc(&server.uri()).await;

        Mock::given(method("POST"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "error": { "message": "boom" }
            })))
            .mount(&server)
            .await;

        let err = get_evm_transaction_receipt_status(&wallet, "0xdead")
            .await
            .unwrap_err();
        assert!(err.to_string().contains("RPC error"));
    }

    #[tokio::test]
    async fn get_evm_transaction_receipt_status_errors_for_malformed_body() {
        let server = MockServer::start().await;
        let wallet = wallet_with_rpc(&server.uri()).await;

        Mock::given(method("POST"))
            .respond_with(ResponseTemplate::new(200).set_body_string("not-json"))
            .mount(&server)
            .await;

        let err = get_evm_transaction_receipt_status(&wallet, "0xdead")
            .await
            .unwrap_err();
        assert!(err.to_string().contains("RPC response parse failed"));
    }
}
