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

use std::str::FromStr;

use async_trait::async_trait;
use base64::Engine as _;
#[cfg(feature = "rpc-client")]
use reqwest::{Client, Url};
#[cfg(feature = "rpc-client")]
use rialo_api_types::{
    messages::{
        get_account_info::{AccountInfoResponse, GetAccountInfoConfig, GetAccountInfoRequest},
        get_accounts_by_owner::{
            GetAccountsByOwnerConfig, GetAccountsByOwnerRequest, GetAccountsByOwnerResponse,
        },
        get_all_accounts::{
            GetAllAccountsConfig as ApiAllAccountsConfig, GetAllAccountsRequest,
            GetAllAccountsResponse as ApiAllAccountsResponse,
        },
        get_balance::GetBalanceResponse,
        get_block::{GetBlockConfig as ApiBlockConfig, GetBlockRequest},
        get_block_height::RequestBlockHeightConfig,
        get_blocks::GetBlocksRequest,
        get_cluster_nodes::GetClusterNodesResponse as ApiClusterNodesResponse,
        get_connected_full_nodes::GetConnectedFullNodesResponse as ApiConnectedFullNodesResponse,
        get_connected_validators::GetConnectedValidatorsRequest,
        get_epoch_info::{
            EpochInfoResponse as ApiEpochInfoResponse, GetEpochInfoConfig, GetEpochInfoRequest,
        },
        get_fee_for_message::GetFeeForMessageRequest,
        get_fee_for_message::GetFeeForMessageResponse as ApiFeeResponse,
        get_minimum_balance_for_rent_exemption::GetMinimumBalanceForRentExemptionRequest,
        get_multiple_accounts::{GetMultipleAccountsRequest, MultipleAccountsResponse},
        get_rex_missed_duties::{GetRexMissedDutiesRequest, GetRexMissedDutiesResponse},
        get_rex_requests::{GetRexRequestsRequest, GetRexRequestsResponse},
        get_signature_statuses::{GetSignatureStatusesRequest, GetSignatureStatusesResponse},
        get_signatures_for_address::{
            GetSignaturesForAddressConfig as ApiSignaturesConfig, GetSignaturesForAddressRequest,
        },
        get_stake_account::{GetStakeAccountRequest, GetStakeAccountResponse},
        get_subscription::GetSubscriptionResponse as ApiSubscriptionResponse,
        get_token_account_balance::{
            GetTokenAccountBalanceRequest, GetTokenAccountBalanceResponse,
        },
        get_transaction::GetTransactionResponse,
        get_transaction_count::GetTransactionCountResponse,
        get_transactions::{
            GetTransactionsConfig as ApiTransactionsConfig, GetTransactionsRequest,
            GetTransactionsResponse,
        },
        get_triggered_transactions::GetTriggeredTransactionsResponse as ApiTriggeredTxResponse,
        get_validator_accounts::{
            GetValidatorAccountsRequest as ApiValidatorAccountsRequest,
            GetValidatorAccountsResponse,
        },
        get_workflow_lineage::GetWorkflowLineageRequest as ApiWorkflowLineageRequest,
        is_blockhash_valid::IsBlockhashValidResponse as ApiBlockhashValidResponse,
        send_transaction::SendTransactionConfig,
        submit_epoch_change::{
            EpochConsensusConfigRequest as ApiConsensusConfig,
            SubmitEpochChangeRequest as ApiSubmitEpochChangeRequest,
            ValidatorInfoRequest as ApiValidatorInfoRequest,
        },
    },
    parameters::*,
};
#[cfg(feature = "rpc-client")]
use rialo_shared_types::AccountInfoResponseValue;
#[cfg(feature = "distributed-tracing")]
use rialo_telemetry::{apply_trace_headers_to_reqwest, inject_trace_headers};
#[cfg(feature = "rpc-client")]
use serde_json::{json, Value};

// API request/response types — used internally for JSON-RPC communication.
#[cfg(feature = "rpc-client")]
use crate::constants::RPC_VERSION;
#[cfg(feature = "rpc-client")]
use crate::error::{Result, RialoError};
#[cfg(feature = "rpc-client")]
use crate::generated::rpc_client::RpcClient;
// All types from the generated WIT spec — the canonical type surface.
#[cfg(feature = "rpc-client")]
use crate::generated::types::{
    AccountInfo, AllAccountsEntry, BlockInfo, ClusterNodeInfo, ConfigHashPrefix,
    ConfirmTransactionOptions, ConfirmedTransaction, ConnectedNode, EpochInfo, FeeResponse,
    GetAllAccountsConfig, GetBlockConfig, GetSignaturesForAddressConfig, GetTransactionsConfig,
    GetValidatorAccountsRequest, GetWorkflowLineageRequest, GetWorkflowLineageResponse, Hash,
    IsBlockhashValidResponse, Kelvin, OptionalAccountInfo, OwnerAccount, PaginationInfo, PublicKey,
    RexInfoAndDuties, SecretSharingPubkey, SendAndConfirmOptions, SendTransactionOptions,
    Signature, SignatureInfo, SignatureStatus, StakeAccountInfo, SubmitEpochChangeRequest,
    SubmitEpochChangeResponse, Subscription, TokenBalance, TransactionInfo, TransactionResponse,
    TriggeredTransaction, ValidatorAccountInfo, ValidatorHealth,
};
pub use crate::rpc::no_wasm::*;
#[cfg(feature = "rpc-client")]
use crate::rpc::traits::GenericRpcCaller;

/// `HttpRpcClient` implements JSON-RPC communication with the Rialo blockchain
/// over HTTP.
///
/// This client handles serialization, deserialization, and error handling for all
/// RPC requests to the blockchain.
#[cfg(feature = "rpc-client")]
pub struct HttpRpcClient {
    /// The URL of the RPC endpoint
    url: String,
    /// HTTP client used for making requests
    client: Client,
    /// Atomic counter for generating unique request IDs
    id: std::sync::atomic::AtomicU64,
}

#[cfg(feature = "rpc-client")]
impl HttpRpcClient {
    /// Creates a new HTTP RPC client connected to the specified URL.
    ///
    /// Certificate validation is automatically disabled for localhost URLs
    /// (localhost, 127.0.0.1, ::1) and enabled for all other URLs.
    ///
    /// # Arguments
    ///
    /// * `url` - The URL of the Rialo RPC endpoint
    ///
    /// # Returns
    ///
    /// A new `HttpRpcClient` instance
    pub fn new(url: String) -> Self {
        let danger_accept_invalid_certs = Self::is_localhost(&url);
        Self::new_with_config(url, danger_accept_invalid_certs)
    }

    /// Creates a new HTTP RPC client with explicit TLS configuration.
    ///
    /// # Arguments
    ///
    /// * `url` - The URL of the Rialo RPC endpoint
    /// * `danger_accept_invalid_certs` - Whether to accept invalid TLS certificates
    ///   (DANGEROUS - only use for testing with self-signed certificates).
    ///   Note: This parameter is ignored on wasm32 targets.
    ///
    /// # Returns
    ///
    /// A new `HttpRpcClient` instance
    pub fn new_with_config(
        url: String,
        #[allow(unused_variables)] danger_accept_invalid_certs: bool,
    ) -> Self {
        let client = {
            #[cfg(not(target_arch = "wasm32"))]
            let client = Client::builder()
                .danger_accept_invalid_certs(danger_accept_invalid_certs)
                .build()
                .expect("Failed to build HTTP client");

            #[cfg(target_arch = "wasm32")]
            let client = Client::builder()
                .build()
                .expect("Failed to build HTTP client");

            client
        };

        Self {
            url,
            client,
            id: std::sync::atomic::AtomicU64::new(1),
        }
    }

    /// Checks if a URL points to localhost.
    ///
    /// Returns true for:
    /// - localhost
    /// - 127.0.0.1
    /// - ::1 (IPv6 localhost)
    ///
    /// # Arguments
    ///
    /// * `url` - The URL to check
    ///
    /// # Returns
    ///
    /// `true` if the URL is a localhost URL, `false` otherwise
    pub fn is_localhost(url: &str) -> bool {
        if let Ok(parsed) = Url::parse(url) {
            if let Some(host) = parsed.host_str() {
                return host == "localhost"
                    || host == "127.0.0.1"
                    || host == "::1"
                    || host == "[::1]";
            }
        }
        false
    }

    /// Generates the next unique request ID for RPC calls.
    ///
    /// # Returns
    ///
    /// A unique u64 ID for the next RPC request
    fn next_id(&self) -> u64 {
        self.id.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
    }

    /// Sends an RPC request with parameters as a JSON value.
    ///
    /// # Arguments
    ///
    /// * `method` - The RPC method name to call
    /// * `params` - The parameters as a JSON value
    ///
    /// # Returns
    ///
    /// The response as a JSON value or an error
    /// Converts an API `AccountInfoResponseValue` into a generated `AccountInfo`.
    ///
    /// Handles: owner string → PublicKey, base64-encoded data → raw bytes,
    /// kelvin → balance field rename.
    fn convert_api_account(api: AccountInfoResponseValue) -> Result<AccountInfo> {
        let owner = PublicKey::from_str(&api.owner).map_err(|e| {
            RialoError::Rpc(crate::error::RpcErrorDetails::from_message(format!(
                "Invalid owner public key: {e}"
            )))
        })?;

        Ok(AccountInfo {
            kelvin: api.kelvin,
            owner,
            data: api.data.clone(),
            executable: api.executable,
            rent_epoch: api.rent_epoch,
            space: api.space as u64,
        })
    }

    pub async fn call_with_json(
        &self,
        method: &str,
        params: serde_json::Value,
    ) -> Result<serde_json::Value> {
        self.call(method, params).await
    }
}

#[cfg(feature = "rpc-client")]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl GenericRpcCaller for HttpRpcClient {
    /// Sends a generic RPC request to the Rialo blockchain and parses the response.
    ///
    /// # Arguments
    ///
    /// * `method` - The RPC method name to call
    /// * `params` - The parameters to pass to the method
    ///
    /// # Returns
    ///
    /// The deserialized response result or an error if the request failed
    ///
    /// # Errors
    ///
    /// Returns `RialoError::Rpc` if the request fails, receives an error response,
    /// or if the response cannot be properly parsed
    #[tracing::instrument(skip(self, params))]
    async fn call<T, R>(&self, method: &str, params: T) -> Result<R>
    where
        T: serde::Serialize + NoWasmSend,
        R: for<'de> serde::Deserialize<'de> + NoWasmSend,
    {
        let id = self.next_id();
        let request = json!({
            "jsonrpc": RPC_VERSION,
            "id": id,
            "method": method,
            "params": params,
        });

        log::trace!(
            "Calling the equivalent of: curl -X POST {} -H 'Content-Type: application/json' -d '{}'",
            self.url,
            serde_json::to_string(&request).unwrap_or_default()
        );

        // Build request with trace context propagation
        #[allow(unused_mut)]
        let mut request_builder = self.client.post(&self.url).json(&request);

        #[cfg(feature = "distributed-tracing")]
        {
            let trace_headers = inject_trace_headers();
            request_builder = apply_trace_headers_to_reqwest(request_builder, trace_headers);
        }

        let response = request_builder.send().await?;

        if !response.status().is_success() {
            let status = response.status();
            let body = response
                .text()
                .await
                .unwrap_or_else(|_| "Unable to read response body".to_string());
            log::error!("HTTP request failed with status {status} and body: {body}");

            // Try to parse JSON-RPC error from the response body
            let error_details = if let Ok(json) = serde_json::from_str::<Value>(&body) {
                if let Some(error) = json.get("error") {
                    let code = error.get("code").and_then(|c| c.as_i64()).unwrap_or(0);
                    let message = error
                        .get("message")
                        .and_then(|m| m.as_str())
                        .unwrap_or("Unknown error")
                        .to_string();
                    crate::error::RpcErrorDetails::new(Some(status.as_u16()), code, message)
                } else {
                    crate::error::RpcErrorDetails::new(
                        Some(status.as_u16()),
                        status.as_u16() as i64,
                        body,
                    )
                }
            } else {
                crate::error::RpcErrorDetails::new(
                    Some(status.as_u16()),
                    status.as_u16() as i64,
                    body,
                )
            };

            return Err(RialoError::Rpc(error_details));
        }

        let json: Value = response.json().await?;

        if let Some(error) = json.get("error") {
            // Enhanced error handling - extract more details from the error object
            let code = error.get("code").and_then(|c| c.as_i64()).unwrap_or(0);
            let mut message = error
                .get("message")
                .and_then(|m| m.as_str())
                .unwrap_or("Unknown error")
                .to_string();

            // Include data field in message if present
            if let Some(data) = error.get("data") {
                if !data.to_string().is_empty() && *data != "null" {
                    message.push_str(&format!(" (data: {data})"));
                }
            }

            let error_details = crate::error::RpcErrorDetails::new(None, code, message);
            return Err(RialoError::Rpc(error_details));
        }

        let result = json.get("result").ok_or_else(|| {
            let error_details = crate::error::RpcErrorDetails::from_message(
                "Missing 'result' field in response".to_string(),
            );
            RialoError::Rpc(error_details)
        })?;

        serde_json::from_value(result.clone()).map_err(|e| {
            let error_details =
                crate::error::RpcErrorDetails::from_message(format!("Failed to parse result: {e}"));
            RialoError::Rpc(error_details)
        })
    }
}

#[cfg(feature = "rpc-client")]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl RpcClient for HttpRpcClient {
    async fn get_balance(&self, pubkey: &PublicKey) -> Result<Kelvin> {
        let params = GetBalanceRequest::new(pubkey.to_string());
        let result: GetBalanceResponse = self.call("getBalance", [params]).await?;

        Ok(result.value)
    }

    async fn send_transaction(
        &self,
        transaction: &[u8],
        options: Option<SendTransactionOptions>,
    ) -> Result<Signature> {
        let opts = options.unwrap_or(SendTransactionOptions {
            skip_preflight: true,
            max_retries: 0,
            wait_for_execution: false,
        });
        // Convert generated SendTransactionOptions → API SendTransactionConfig
        let config = SendTransactionConfig {
            skip_preflight: opts.skip_preflight,
            max_retries: Some(opts.max_retries as usize),
            wait_for_execution: opts.wait_for_execution,
            ..Default::default()
        };
        #[allow(clippy::disallowed_methods)]
        let encoded = base64::engine::general_purpose::STANDARD.encode(transaction);
        let params = SendTransactionRequest::new(encoded, config);
        let signature_str: String = self.call("sendTransaction", [params]).await?;
        Signature::from_str(&signature_str).map_err(|e| {
            let error_details = crate::error::RpcErrorDetails::from_message(format!(
                "Invalid signature format: {e}"
            ));
            RialoError::Rpc(error_details)
        })
    }

    async fn get_account_info(&self, pubkey: &PublicKey) -> Result<AccountInfo> {
        let params = GetAccountInfoRequest {
            address: pubkey.to_string(),
            config: Some(GetAccountInfoConfig {
                data_slice: None,
                min_context_slot: None,
            }),
        };
        let result: AccountInfoResponse = self.call("getAccountInfo", [params]).await?;

        let api_value = result.value.ok_or_else(|| {
            RialoError::Rpc(crate::error::RpcErrorDetails::from_message(format!(
                "Account does not exist: {pubkey}"
            )))
        })?;

        Self::convert_api_account(api_value)
    }

    async fn get_block_height(&self) -> Result<u64> {
        self.get_block_height_with_config(None).await
    }

    async fn get_block_height_with_config(&self, min_context_slot: Option<u64>) -> Result<u64> {
        let config = RequestBlockHeightConfig { min_context_slot };
        self.call("getBlockHeight", [config]).await
    }

    async fn get_transaction(&self, signature: &Signature) -> Result<TransactionResponse> {
        let params = GetTransactionRequest::new(signature.to_string());
        let response: GetTransactionResponse = self.call("getTransaction", [params]).await?;

        // Convert API types → CDK generated types via TryFrom/From impls
        let transaction: crate::generated::types::TransactionData =
            response.transaction.try_into()?;
        let meta: crate::generated::types::TransactionStatusMetadata = response.meta.into();

        Ok(TransactionResponse {
            block_height: response.block_height,
            meta,
            transaction,
            block_time: response.block_time,
        })
    }

    async fn get_transaction_count(&self) -> Result<u64> {
        let params = GetTransactionCountRequest::new();
        let response: GetTransactionCountResponse =
            self.call("getTransactionCount", [params]).await?;
        Ok(response.value)
    }

    async fn get_minimum_balance_for_rent_exemption(&self, size: u64) -> Result<u64> {
        let params = GetMinimumBalanceForRentExemptionRequest { data_length: size };
        self.call("getMinimumBalanceForRentExemption", [params])
            .await
    }

    async fn get_signature_statuses(
        &self,
        signatures: &[Signature],
    ) -> Result<Vec<Option<SignatureStatus>>> {
        let signature_strings: Vec<String> = signatures.iter().map(|s| s.to_string()).collect();
        let params = GetSignatureStatusesRequest {
            signatures: signature_strings,
            config: None,
        };
        let response: GetSignatureStatusesResponse =
            self.call("getSignatureStatuses", [params]).await?;

        // Convert from API response format to CDK compatibility format
        let signature_statuses: Vec<Option<SignatureStatus>> = response
            .value
            .into_iter()
            .map(|status| {
                Some(SignatureStatus {
                    slot: response.context.slot,
                    err: status.err.map(|e| e.to_string()),
                    executed: status.executed,
                })
            })
            .collect();

        Ok(signature_statuses)
    }

    async fn get_subscription(&self, subscriber: &PublicKey, nonce: &str) -> Result<Subscription> {
        let params =
            GetSubscriptionRequest::new(Some(subscriber.to_string()), Some(nonce.to_string()));
        let response: ApiSubscriptionResponse = self.call("getSubscription", [params]).await?;

        // Convert API subscription → generated Subscription field-by-field
        let sub = response.subscription;
        // Convert rialo_shared_types::SubscriptionKind → generated::types::SubscriptionKind
        let kind = match sub.kind {
            rialo_shared_types::SubscriptionKind::Persistent => {
                crate::generated::types::SubscriptionKind::Persistent
            }
            rialo_shared_types::SubscriptionKind::OneShot => {
                crate::generated::types::SubscriptionKind::OneShot
            }
        };
        Ok(Subscription {
            kind,
            topic: sub.topic,
            instructions: sub
                .instructions
                .into_iter()
                .map(|ix| crate::generated::types::SubscriptionInstruction {
                    program_id: ix.program_id,
                    accounts: ix
                        .accounts
                        .into_iter()
                        .map(|a| crate::generated::types::SubscriptionAccountMeta {
                            pubkey: a.pubkey,
                            is_signer: a.is_signer,
                            is_writable: a.is_writable,
                        })
                        .collect(),
                    data: ix.data,
                })
                .collect(),
            subscriber: sub.subscriber,
            event_account: sub.event_account,
            timestamp_range: sub.timestamp_range,
        })
    }

    async fn get_triggered_transactions(
        &self,
        subscription_account: &PublicKey,
        limit: Option<u32>,
    ) -> Result<Vec<TriggeredTransaction>> {
        let mut params: Vec<Value> = vec![Value::String(subscription_account.to_string())];
        if let Some(lim) = limit {
            params.push(Value::String(lim.to_string()));
        }

        let response: ApiTriggeredTxResponse =
            self.call("getTriggeredTransactions", params).await?;

        // Convert API TriggeredTransaction → generated TriggeredTransaction field-by-field
        let transactions = response
            .transactions
            .into_iter()
            .map(|t| TriggeredTransaction {
                signature: t.signature,
                block_number: t.block_number,
            })
            .collect();

        Ok(transactions)
    }

    async fn request_airdrop(&self, pubkey: &PublicKey, amount: Kelvin) -> Result<Signature> {
        let params = RequestAirdropRequest::new(pubkey.to_string(), amount);
        let signature_str: String = self.call("requestAirdrop", [params]).await?;
        Signature::from_str(&signature_str).map_err(|e| {
            let error_details = crate::error::RpcErrorDetails::from_message(format!(
                "Invalid signature format: {e}"
            ));
            RialoError::Rpc(error_details)
        })
    }

    async fn get_signatures_for_address(
        &self,
        address: &PublicKey,
        config: Option<GetSignaturesForAddressConfig>,
    ) -> Result<Vec<SignatureInfo>> {
        // Convert generated config → API config field-by-field
        let api_config: Option<ApiSignaturesConfig> = config.map(|c| ApiSignaturesConfig {
            limit: c.limit.map(|l| l as u16),
            before: c.before,
            until: c.until,
        });

        let params = GetSignaturesForAddressRequest {
            address: address.to_string(),
            config: api_config,
        };
        self.call("getSignaturesForAddress", [params]).await
    }

    async fn get_epoch_info(&self) -> Result<EpochInfo> {
        let params = GetEpochInfoRequest {
            config: Some(GetEpochInfoConfig {
                min_context_slot: None,
            }),
        };
        // EpochInfo fields match API JSON (camelCase handled by serde on API type)
        let response: ApiEpochInfoResponse = self.call("getEpochInfo", [params]).await?;
        Ok(EpochInfo {
            epoch: response.epoch,
            slot_index: response.slot_index,
            slots_in_epoch: response.slots_in_epoch,
            absolute_slot: response.absolute_slot,
            block_height: response.block_height,
            transaction_count: response.transaction_count,
        })
    }

    async fn get_fee_for_message(&self, message: &str) -> Result<FeeResponse> {
        let params = GetFeeForMessageRequest {
            message: message.to_string(),
        };
        let response: ApiFeeResponse = self.call("getFeeForMessage", [params]).await?;
        Ok(FeeResponse {
            value: response.value,
        })
    }

    async fn get_multiple_accounts(
        &self,
        pubkeys: &[PublicKey],
    ) -> Result<Vec<OptionalAccountInfo>> {
        let pubkey_strings: Vec<String> = pubkeys.iter().map(|pk| pk.to_string()).collect();
        let params = GetMultipleAccountsRequest {
            addresses: pubkey_strings,
        };
        let response: MultipleAccountsResponse = self.call("getMultipleAccounts", [params]).await?;

        // Convert each API AccountInfoResponseValue → generated AccountInfo field-by-field
        response
            .value
            .into_iter()
            .map(|opt| match opt {
                Some(api_account) => Self::convert_api_account(api_account).map(Some),
                None => Ok(None),
            })
            .collect()
    }

    async fn get_workflow_lineage(
        &self,
        request: &GetWorkflowLineageRequest,
    ) -> Result<GetWorkflowLineageResponse> {
        // Convert generated request → API request field-by-field
        let api_request = ApiWorkflowLineageRequest {
            signature: request.signature.clone(),
            max_depth: request.max_depth.map(|d| d as usize),
            include_events: request.include_events,
        };
        self.call("getWorkflowLineage", [api_request]).await
    }

    async fn get_transactions(
        &self,
        config: Option<GetTransactionsConfig>,
    ) -> Result<Vec<TransactionInfo>> {
        // Convert generated config → API config field-by-field
        let api_config: Option<ApiTransactionsConfig> = config.map(|c| ApiTransactionsConfig {
            limit: c.limit.map(|l| l as u16),
            before: c.before,
            encoding: None,
            max_supported_transaction_version: None,
        });
        let params = GetTransactionsRequest { config: api_config };
        let response: GetTransactionsResponse = self.call("getTransactions", [params]).await?;

        // Convert API TransactionInfo → generated TransactionInfo field-by-field
        let transactions = response
            .value
            .into_iter()
            .map(|t| TransactionInfo {
                signature: t.signature,
                slot: t.slot,
                block_time: t.block_time,
                err: t.err.map(|e| e.to_string()),
                version: t.version,
            })
            .collect();

        Ok(transactions)
    }

    async fn is_blockhash_valid(&self, blockhash: &Hash) -> Result<IsBlockhashValidResponse> {
        let params = IsBlockhashValidRequest::new(blockhash.to_string());
        let response: ApiBlockhashValidResponse = self.call("isBlockhashValid", [params]).await?;

        Ok(IsBlockhashValidResponse {
            slot: response.context.slot,
            is_valid: response.value,
        })
    }

    async fn get_health(&self) -> Result<String> {
        let params = GetHealthRequest::new();
        self.call("getHealth", [params]).await
    }

    async fn get_validator_health(&self) -> Result<ValidatorHealth> {
        let params = GetValidatorHealthRequest::new();
        self.call("getValidatorHealth", [params]).await
    }

    async fn get_connected_full_nodes(&self) -> Result<Vec<ConnectedNode>> {
        let params = GetConnectedFullNodesRequest::new();
        let response: ApiConnectedFullNodesResponse =
            self.call("getConnectedFullNodes", [params]).await?;

        // Convert API tuples (pubkey, millis) → generated ConnectedNode field-by-field
        let nodes = response
            .pubkeys_and_millis
            .into_iter()
            .map(|(public_key, connected_ms)| ConnectedNode {
                public_key,
                connected_ms: connected_ms as u64,
            })
            .collect();
        Ok(nodes)
    }

    async fn get_secret_sharing_pubkey(&self) -> Result<SecretSharingPubkey> {
        // Deserialize to Value to extract the hex string directly (pubkey field is private)
        let response: Value = self.call("getSecretSharingPubkey", [Value::Null]).await?;
        let pubkey_str = response
            .get("pubkey")
            .and_then(|v| v.as_str())
            .ok_or_else(|| {
                RialoError::Rpc(crate::error::RpcErrorDetails::from_message(
                    "Missing pubkey field in getSecretSharingPubkey response".to_string(),
                ))
            })?;
        Ok(SecretSharingPubkey {
            public_key: pubkey_str.to_string(),
        })
    }

    async fn send_admin_transaction(
        &self,
        request: &SubmitEpochChangeRequest,
    ) -> Result<SubmitEpochChangeResponse> {
        // Convert generated request → API request field-by-field
        let api_validators: Vec<ApiValidatorInfoRequest> = request
            .validators
            .iter()
            .map(|v| {
                Ok(ApiValidatorInfoRequest {
                    stake: v.stake,
                    consensus_address: rialo_types::Multiaddr::from_str(&v.consensus_address)
                        .map_err(|e| {
                            RialoError::Rpc(crate::error::RpcErrorDetails::from_message(format!(
                                "Invalid consensus address: {e}"
                            )))
                        })?,
                    state_sync_address: rialo_types::Multiaddr::from_str(&v.state_sync_address)
                        .map_err(|e| {
                            RialoError::Rpc(crate::error::RpcErrorDetails::from_message(format!(
                                "Invalid state sync address: {e}"
                            )))
                        })?,
                    hostname: v.hostname.clone(),
                    authority_key: v.authority_key.clone(),
                    protocol_key: v.protocol_key.clone(),
                    network_key: v.network_key.clone(),
                    signing_key: v.signing_key.clone(),
                })
            })
            .collect::<Result<Vec<_>>>()?;

        let api_consensus_config = request
            .consensus_config
            .as_ref()
            .map(|c| ApiConsensusConfig {
                num_leaders_per_round: c.num_leaders_per_round.map(|n| n as usize),
                max_transactions_in_block_bytes: c.max_transactions_in_block_bytes,
                max_num_transactions_in_block: c.max_num_transactions_in_block,
                max_transaction_size_bytes: c.max_transaction_size_bytes,
                gc_depth: c.gc_depth,
                zstd_compression: c.zstd_compression,
            });

        let api_request = ApiSubmitEpochChangeRequest {
            new_epoch: request.new_epoch,
            validators: api_validators,
            consensus_config: api_consensus_config,
        };
        self.call("submitEpochChange", [api_request]).await
    }

    async fn get_accounts_by_owner(
        &self,
        owner: &PublicKey,
        filter: Option<crate::generated::types::AccountFilter>,
        config: Option<crate::generated::types::GetAccountsByOwnerConfig>,
    ) -> Result<(Vec<OwnerAccount>, Option<PaginationInfo>)> {
        // Convert generated AccountFilter → API AccountFilter
        let api_filter = match filter {
            Some(crate::generated::types::AccountFilter::TokenAccounts(mint)) => {
                rialo_api_types::messages::get_accounts_by_owner::AccountFilter::TokenAccounts {
                    mint,
                }
            }
            _ => rialo_api_types::messages::get_accounts_by_owner::AccountFilter::ProgramAccounts,
        };
        // Convert generated config → API config
        let api_config = config
            .map(|c| {
                let mut cfg = GetAccountsByOwnerConfig::new();
                cfg.limit = c.limit.map(|l| l as usize);
                cfg.after = c.after;
                cfg
            })
            .unwrap_or_default();
        let params =
            GetAccountsByOwnerRequest::with_config(owner.to_string(), api_filter, api_config);
        let response: GetAccountsByOwnerResponse =
            self.call("getAccountsByOwner", [params]).await?;

        // Convert each API OwnerAccount → generated OwnerAccount field-by-field
        let accounts: Vec<OwnerAccount> = response
            .value
            .into_iter()
            .map(|a| {
                let pubkey = PublicKey::from_str(&a.pubkey).map_err(|e| {
                    RialoError::Rpc(crate::error::RpcErrorDetails::from_message(format!(
                        "Invalid account pubkey: {e}"
                    )))
                })?;
                let account = Self::convert_api_account(a.account)?;
                Ok(OwnerAccount { pubkey, account })
            })
            .collect::<Result<Vec<_>>>()?;

        // Convert API PaginationInfo → generated PaginationInfo field-by-field
        let pagination = response.pagination.map(|p| PaginationInfo {
            has_more: p.has_more,
            next_cursor: p.next_cursor,
        });

        Ok((accounts, pagination))
    }
    async fn get_config_hash_prefix(&self) -> Result<ConfigHashPrefix> {
        use rialo_api_types::messages::get_recent_validator_config_hash::{
            GetRecentValidatorConfigHashRequest, GetRecentValidatorConfigHashResponse,
        };
        let params = GetRecentValidatorConfigHashRequest::default();
        let response: GetRecentValidatorConfigHashResponse =
            self.call("getRecentValidatorConfigHash", [params]).await?;
        Ok(response.config_hash_prefix)
    }

    /// Polls until a transaction is confirmed or times out.
    /// Ported from TypeScript TransactionRpcClient.confirmTransaction.
    async fn confirm_transaction(
        &self,
        sig: &str,
        options: Option<ConfirmTransactionOptions>,
    ) -> Result<ConfirmedTransaction> {
        let max_retries = options.as_ref().map(|o| o.max_retries).unwrap_or(10);
        let retry_delay_ms = options.as_ref().map(|o| o.retry_delay_ms).unwrap_or(200);

        let sig_obj = Signature::from_str(sig).map_err(|e| {
            RialoError::Rpc(crate::error::RpcErrorDetails::from_message(format!(
                "Invalid signature: {e}"
            )))
        })?;

        for attempt in 0..max_retries {
            if attempt > 0 {
                #[cfg(not(target_arch = "wasm32"))]
                tokio::time::sleep(std::time::Duration::from_millis(retry_delay_ms as u64)).await;
                #[cfg(target_arch = "wasm32")]
                gloo_timers::future::TimeoutFuture::new(retry_delay_ms).await;
            }

            match self.get_transaction(&sig_obj).await {
                Ok(tx_info) => {
                    if let Some(err) = tx_info.meta.err {
                        return Ok(ConfirmedTransaction {
                            signature: sig.to_string(),
                            executed: false,
                            err: Some(err),
                        });
                    }
                    if tx_info.block_height > 0 {
                        return Ok(ConfirmedTransaction {
                            signature: sig.to_string(),
                            executed: true,
                            err: None,
                        });
                    }
                }
                Err(_) => {
                    // Transaction not found yet, continue polling
                    continue;
                }
            }
        }

        Err(RialoError::Rpc(
            crate::error::RpcErrorDetails::from_message(format!(
                "Transaction confirmation timed out after {max_retries} attempts: {sig}"
            )),
        ))
    }

    /// Sends a transaction and waits for confirmation.
    async fn send_and_confirm_transaction(
        &self,
        transaction: &[u8],
        options: Option<SendAndConfirmOptions>,
    ) -> Result<ConfirmedTransaction> {
        let send_opts = options.as_ref().map(|o| SendTransactionOptions {
            skip_preflight: o.skip_preflight,
            max_retries: o.max_retries,
            wait_for_execution: false,
        });
        let signature = self.send_transaction(transaction, send_opts).await?;

        let confirm_opts = options.map(|o| ConfirmTransactionOptions {
            max_retries: o.confirm_max_retries,
            retry_delay_ms: o.confirm_retry_delay_ms,
        });
        self.confirm_transaction(&signature.to_string(), confirm_opts)
            .await
    }

    /// Requests an airdrop and waits for confirmation.
    async fn request_airdrop_and_confirm(
        &self,
        pubkey: &PublicKey,
        amount: Kelvin,
    ) -> Result<ConfirmedTransaction> {
        let signature = self.request_airdrop(pubkey, amount).await?;
        self.confirm_transaction(&signature.to_string(), None).await
    }

    async fn get_all_accounts(
        &self,
        config: Option<GetAllAccountsConfig>,
    ) -> Result<Vec<AllAccountsEntry>> {
        let api_config = config.map(|c| ApiAllAccountsConfig {
            encoding: c.encoding,
            include_data: c.include_data,
        });
        let params = GetAllAccountsRequest { config: api_config };
        let response: ApiAllAccountsResponse = self.call("getAllAccounts", [params]).await?;

        // Convert each API AllAccountsEntry → generated AllAccountsEntry
        response
            .value
            .into_iter()
            .map(|entry| {
                let account = Self::convert_api_account(entry.account)?;
                Ok(AllAccountsEntry {
                    pubkey: entry.pubkey,
                    account,
                })
            })
            .collect()
    }

    async fn get_block(
        &self,
        block_height: u64,
        config: Option<GetBlockConfig>,
    ) -> Result<BlockInfo> {
        let api_config = config.map(|c| ApiBlockConfig {
            transaction_details: c
                .transaction_details
                .map(|s| match s.as_str() {
                    "signatures" => {
                        rialo_api_types::messages::get_block::TransactionDetails::Signatures
                    }
                    "none" => rialo_api_types::messages::get_block::TransactionDetails::None,
                    _ => rialo_api_types::messages::get_block::TransactionDetails::Full,
                })
                .unwrap_or(rialo_api_types::messages::get_block::TransactionDetails::Full),
        });
        let params = GetBlockRequest {
            block_height,
            config: api_config,
        };
        let response: Value = self.call("getBlock", [params]).await?;

        // GetBlockResponse only derives Serialize (not Deserialize), so parse from Value
        let block_value = response.get("value").unwrap_or(&response);

        // Convert transactions if present (via TryFrom<&Value> bridge impl)
        let transactions = block_value
            .get("transactions")
            .and_then(|v| v.as_array())
            .map(|txs| {
                txs.iter()
                    .filter_map(|v| crate::generated::types::TransactionWithMeta::try_from(v).ok())
                    .collect()
            });

        let signatures: Option<Vec<String>> = block_value
            .get("signatures")
            .and_then(|s| serde_json::from_value(s.clone()).ok());

        Ok(BlockInfo {
            blockhash: block_value
                .get("blockhash")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            block_height: block_value
                .get("blockHeight")
                .and_then(|v| v.as_u64())
                .unwrap_or(0),
            block_time: block_value
                .get("blockTime")
                .and_then(|v| v.as_i64())
                .unwrap_or(0),
            transactions,
            signatures,
        })
    }

    async fn get_blocks(&self, start_height: u64, end_height: Option<u64>) -> Result<Vec<u64>> {
        let params = GetBlocksRequest {
            start_slot: start_height,
            end_slot: end_height,
        };
        self.call("getBlocks", [params]).await
    }

    async fn get_stake_account(&self, pubkey: &PublicKey) -> Result<Option<StakeAccountInfo>> {
        let params = GetStakeAccountRequest {
            pubkey: pubkey.to_string(),
        };
        let response: GetStakeAccountResponse = self.call("getStakeAccount", [params]).await?;
        // The API response value is optional — None means not a stake account
        Ok(response.value.map(|s| StakeAccountInfo {
            state: match s.state {
                rialo_api_types::messages::get_stake_account::StakeState::Inactive => {
                    crate::generated::types::StakeState::Inactive
                }
                rialo_api_types::messages::get_stake_account::StakeState::Activating => {
                    crate::generated::types::StakeState::Activating
                }
                rialo_api_types::messages::get_stake_account::StakeState::Active => {
                    crate::generated::types::StakeState::Active
                }
                rialo_api_types::messages::get_stake_account::StakeState::Deactivating => {
                    crate::generated::types::StakeState::Deactivating
                }
                rialo_api_types::messages::get_stake_account::StakeState::Unbonding => {
                    crate::generated::types::StakeState::Unbonding
                }
            },
            kelvins: s.kelvins,
            delegated_balance: s.delegated_balance,
            undelegated_balance: s.undelegated_balance,
            activation_requested: s.activation_requested,
            deactivation_requested: s.deactivation_requested,
            validator: s.validator,
            admin_authority: s.admin_authority,
            withdraw_authority: s.withdraw_authority,
        }))
    }

    async fn get_validator_accounts(
        &self,
        request: &GetValidatorAccountsRequest,
    ) -> Result<Vec<ValidatorAccountInfo>> {
        let params = ApiValidatorAccountsRequest {
            use_frozen: request.use_frozen,
        };
        let response: GetValidatorAccountsResponse =
            self.call("getValidatorAccounts", [params]).await?;

        Ok(response
            .value
            .into_iter()
            .map(|v| ValidatorAccountInfo {
                commission: v.commission,
                pubkey: v.pubkey,
                node_identity: v.node_identity,
                authorized_withdrawer: v.authorized_withdrawer,
                stake_next: v.stake_next,
                stake_current: v.stake_current,
                address: v.address,
            })
            .collect())
    }

    async fn get_token_account_balance(&self, pubkey: &PublicKey) -> Result<TokenBalance> {
        let params = GetTokenAccountBalanceRequest {
            address: pubkey.to_string(),
        };
        let response: GetTokenAccountBalanceResponse =
            self.call("getTokenAccountBalance", [params]).await?;

        Ok(TokenBalance {
            amount: response.value.amount,
            decimals: response.value.decimals,
            ui_amount_string: response.value.ui_amount_string,
        })
    }

    async fn get_rex_requests(
        &self,
        creator: &PublicKey,
        nonce: Option<String>,
    ) -> Result<Vec<RexInfoAndDuties>> {
        let params = GetRexRequestsRequest {
            creator: creator.to_string(),
            nonce,
        };
        let response: GetRexRequestsResponse = self.call("getRexRequests", [params]).await?;

        Ok(response
            .rex_requests
            .into_iter()
            .map(|r| RexInfoAndDuties {
                creator: r.creator,
                nonce: r.nonce,
                is_active: r.is_active,
                description: r.description,
                update_frequency: r.update_frequency,
                starting_timestamp: r.starting_timestamp,
                created_at: r.created_at,
                validators_per_duty: r.validators_per_duty,
                rex_request_delay_ms: r.rex_request_delay_ms,
                duties: r
                    .duties
                    .into_iter()
                    .map(|d| crate::generated::types::RexDuty {
                        target_timestamp: d.target_timestamp,
                        assigned_validators: d.assigned_validators,
                    })
                    .collect(),
            })
            .collect())
    }

    async fn get_rex_missed_duties(&self, creator: &PublicKey, nonce: &str) -> Result<Vec<u64>> {
        let params = GetRexMissedDutiesRequest {
            creator: creator.to_string(),
            nonce: nonce.to_string(),
        };
        let response: GetRexMissedDutiesResponse =
            self.call("getRexMissedDuties", [params]).await?;
        Ok(response.missed_duties_at_proposal_rounds)
    }

    async fn get_cluster_nodes(&self) -> Result<Vec<ClusterNodeInfo>> {
        let response: ApiClusterNodesResponse = self.call("getClusterNodes", [Value::Null]).await?;
        // Convert API ClusterNodeInfo → generated ClusterNodeInfo
        Ok(response
            .nodes
            .into_iter()
            .map(|n| ClusterNodeInfo {
                stake: n.stake,
                address: n.address,
                hostname: n.hostname,
                authority_pubkey: n.authority_pubkey,
                protocol_pubkey: n.protocol_pubkey,
                network_pubkey: n.network_pubkey,
                last_committed_round: n.last_committed_round,
            })
            .collect())
    }

    async fn get_connected_validators(&self) -> Result<Vec<u32>> {
        let params = GetConnectedValidatorsRequest::new();
        self.call("getConnectedValidators", [params]).await
    }
}
