//! RPC Client Abstraction
//!
//! Provides a unified interface for communicating with Synor nodes,
//! whether embedded or external. This allows the wallet to seamlessly
//! switch between connection modes.

use std::sync::Arc;

use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tokio::sync::RwLock;
use tracing::{debug, error, info};

use crate::node::{ConnectionMode, NodeManager, NodeStatus};

/// JSON-RPC 2.0 request
#[derive(Debug, Serialize)]
struct JsonRpcRequest<T> {
    jsonrpc: &'static str,
    method: String,
    params: T,
    id: u64,
}

/// JSON-RPC 2.0 response
#[derive(Debug, Deserialize)]
struct JsonRpcResponse<T> {
    #[allow(dead_code)]
    jsonrpc: String,
    result: Option<T>,
    error: Option<JsonRpcError>,
    #[allow(dead_code)]
    id: u64,
}

/// JSON-RPC 2.0 error
#[derive(Debug, Deserialize)]
struct JsonRpcError {
    code: i32,
    message: String,
    #[allow(dead_code)]
    data: Option<serde_json::Value>,
}

/// Balance information
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Balance {
    /// Address
    pub address: String,
    /// Balance in sompi (smallest unit)
    pub balance: u64,
}

/// UTXO (Unspent Transaction Output)
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Utxo {
    /// Transaction ID
    pub transaction_id: String,
    /// Output index
    pub index: u32,
    /// Amount in sompi
    pub amount: u64,
    /// Script public key (hex)
    pub script_public_key: String,
    /// Block DAA score when created
    pub block_daa_score: u64,
    /// Whether this is a coinbase output
    pub is_coinbase: bool,
}

/// Transaction status
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionStatus {
    /// Transaction ID
    pub transaction_id: String,
    /// Is confirmed
    pub is_confirmed: bool,
    /// Confirmations count
    pub confirmations: u64,
    /// Block hash (if confirmed)
    pub block_hash: Option<String>,
    /// Block time (if confirmed)
    pub block_time: Option<u64>,
}

/// Transaction for broadcasting
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawTransaction {
    /// Transaction version
    pub version: u16,
    /// Inputs
    pub inputs: Vec<TransactionInput>,
    /// Outputs
    pub outputs: Vec<TransactionOutput>,
    /// Lock time
    pub lock_time: u64,
    /// Subnetwork ID
    pub subnetwork_id: String,
    /// Gas
    pub gas: u64,
    /// Payload (hex)
    pub payload: String,
}

/// Transaction input
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionInput {
    /// Previous transaction ID
    pub previous_transaction_id: String,
    /// Previous output index
    pub previous_index: u32,
    /// Signature script (hex)
    pub signature_script: String,
    /// Sequence number
    pub sequence: u64,
}

/// Transaction output
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionOutput {
    /// Amount in sompi
    pub amount: u64,
    /// Script public key (hex)
    pub script_public_key: String,
}

/// Network information
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NetworkInfo {
    /// Network name
    pub network: String,
    /// Is synced
    pub is_synced: bool,
    /// Current block height
    pub block_height: u64,
    /// Blue score
    pub blue_score: u64,
    /// Difficulty
    pub difficulty: f64,
    /// Peer count
    pub peer_count: usize,
    /// Mempool size
    pub mempool_size: u64,
}

/// Fee estimate
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FeeEstimate {
    /// Priority fee rate (sompi per gram)
    pub priority: f64,
    /// Normal fee rate
    pub normal: f64,
    /// Low fee rate
    pub low: f64,
}

/// Unified RPC client for communicating with Synor nodes
pub struct RpcClient {
    /// Node manager for connection handling
    node_manager: Arc<NodeManager>,
    /// HTTP client for external RPC
    http_client: reqwest::Client,
    /// Request ID counter
    request_id: RwLock<u64>,
}

impl RpcClient {
    /// Creates a new RPC client
    pub fn new(node_manager: Arc<NodeManager>) -> Self {
        RpcClient {
            node_manager,
            http_client: reqwest::Client::builder()
                .timeout(std::time::Duration::from_secs(30))
                .build()
                .expect("Failed to build HTTP client"),
            request_id: RwLock::new(0),
        }
    }

    /// Gets the next request ID
    async fn next_id(&self) -> u64 {
        let mut id = self.request_id.write().await;
        *id += 1;
        *id
    }

    /// Gets the current connection mode
    pub async fn connection_mode(&self) -> ConnectionMode {
        self.node_manager.connection_mode().await
    }

    /// Gets the current node status
    pub async fn node_status(&self) -> NodeStatus {
        self.node_manager.status().await
    }

    /// Makes an RPC call to an external node
    async fn call_external<P, R>(&self, http_url: &str, method: &str, params: P) -> crate::Result<R>
    where
        P: Serialize,
        R: DeserializeOwned,
    {
        let id = self.next_id().await;
        let request = JsonRpcRequest {
            jsonrpc: "2.0",
            method: method.to_string(),
            params,
            id,
        };

        debug!(method = %method, id = %id, "Making external RPC call");

        let response = self
            .http_client
            .post(http_url)
            .json(&request)
            .send()
            .await
            .map_err(|e| crate::Error::Rpc(format!("HTTP request failed: {}", e)))?;

        let status = response.status();
        if !status.is_success() {
            return Err(crate::Error::Rpc(format!("HTTP error: {}", status)));
        }

        let rpc_response: JsonRpcResponse<R> = response
            .json()
            .await
            .map_err(|e| crate::Error::Rpc(format!("Failed to parse response: {}", e)))?;

        if let Some(error) = rpc_response.error {
            return Err(crate::Error::Rpc(format!(
                "RPC error {}: {}",
                error.code, error.message
            )));
        }

        rpc_response
            .result
            .ok_or_else(|| crate::Error::Rpc("Empty response".to_string()))
    }

    // ============================================================================
    // Public API Methods
    // ============================================================================

    /// Gets the balance for an address
    pub async fn get_balance(&self, address: &str) -> crate::Result<Balance> {
        let mode = self.node_manager.connection_mode().await;

        match mode {
            ConnectionMode::Disconnected => Err(crate::Error::NotConnected),
            ConnectionMode::External { http_url, .. } => {
                self.call_external(&http_url, "synor_getBalanceByAddress", (address,))
                    .await
            }
            #[cfg(feature = "embedded-node")]
            ConnectionMode::Embedded { .. } => {
                // Direct call to embedded node
                if let Some(node) = self.node_manager.embedded_node().await {
                    // TODO: Implement direct UTXO query from embedded node
                    // For now, use the internal RPC
                    let rpc_addr = format!(
                        "http://{}",
                        node.config().rpc.http_addr
                    );
                    self.call_external(&rpc_addr, "synor_getBalanceByAddress", (address,))
                        .await
                } else {
                    Err(crate::Error::NodeNotRunning)
                }
            }
        }
    }

    /// Gets UTXOs for an address
    pub async fn get_utxos(&self, address: &str) -> crate::Result<Vec<Utxo>> {
        let mode = self.node_manager.connection_mode().await;

        match mode {
            ConnectionMode::Disconnected => Err(crate::Error::NotConnected),
            ConnectionMode::External { http_url, .. } => {
                self.call_external(&http_url, "synor_getUtxosByAddress", (address,))
                    .await
            }
            #[cfg(feature = "embedded-node")]
            ConnectionMode::Embedded { .. } => {
                if let Some(node) = self.node_manager.embedded_node().await {
                    let rpc_addr = format!("http://{}", node.config().rpc.http_addr);
                    self.call_external(&rpc_addr, "synor_getUtxosByAddress", (address,))
                        .await
                } else {
                    Err(crate::Error::NodeNotRunning)
                }
            }
        }
    }

    /// Broadcasts a transaction to the network
    pub async fn broadcast_transaction(&self, tx: &RawTransaction) -> crate::Result<String> {
        let mode = self.node_manager.connection_mode().await;

        match mode {
            ConnectionMode::Disconnected => Err(crate::Error::NotConnected),
            ConnectionMode::External { http_url, .. } => {
                self.call_external(&http_url, "synor_submitTransaction", (tx,))
                    .await
            }
            #[cfg(feature = "embedded-node")]
            ConnectionMode::Embedded { .. } => {
                if let Some(node) = self.node_manager.embedded_node().await {
                    let rpc_addr = format!("http://{}", node.config().rpc.http_addr);
                    self.call_external(&rpc_addr, "synor_submitTransaction", (tx,))
                        .await
                } else {
                    Err(crate::Error::NodeNotRunning)
                }
            }
        }
    }

    /// Gets transaction status
    pub async fn get_transaction_status(&self, txid: &str) -> crate::Result<TransactionStatus> {
        let mode = self.node_manager.connection_mode().await;

        match mode {
            ConnectionMode::Disconnected => Err(crate::Error::NotConnected),
            ConnectionMode::External { http_url, .. } => {
                self.call_external(&http_url, "synor_getTransaction", (txid,))
                    .await
            }
            #[cfg(feature = "embedded-node")]
            ConnectionMode::Embedded { .. } => {
                if let Some(node) = self.node_manager.embedded_node().await {
                    let rpc_addr = format!("http://{}", node.config().rpc.http_addr);
                    self.call_external(&rpc_addr, "synor_getTransaction", (txid,))
                        .await
                } else {
                    Err(crate::Error::NodeNotRunning)
                }
            }
        }
    }

    /// Gets network information
    pub async fn get_network_info(&self) -> crate::Result<NetworkInfo> {
        let mode = self.node_manager.connection_mode().await;

        match mode {
            ConnectionMode::Disconnected => Err(crate::Error::NotConnected),
            ConnectionMode::External { http_url, .. } => {
                self.call_external(&http_url, "synor_getInfo", ())
                    .await
            }
            #[cfg(feature = "embedded-node")]
            ConnectionMode::Embedded { .. } => {
                if let Some(node) = self.node_manager.embedded_node().await {
                    // Build network info from embedded node directly
                    let info = node.info().await;
                    Ok(NetworkInfo {
                        network: info.network,
                        is_synced: !info.is_syncing,
                        block_height: info.block_height,
                        blue_score: info.blue_score,
                        difficulty: 0.0, // TODO: Get from consensus
                        peer_count: info.peer_count,
                        mempool_size: 0, // TODO: Get from mempool
                    })
                } else {
                    Err(crate::Error::NodeNotRunning)
                }
            }
        }
    }

    /// Gets fee estimate
    pub async fn get_fee_estimate(&self) -> crate::Result<FeeEstimate> {
        let mode = self.node_manager.connection_mode().await;

        match mode {
            ConnectionMode::Disconnected => Err(crate::Error::NotConnected),
            ConnectionMode::External { http_url, .. } => {
                self.call_external(&http_url, "synor_estimateFee", ())
                    .await
            }
            #[cfg(feature = "embedded-node")]
            ConnectionMode::Embedded { .. } => {
                if let Some(node) = self.node_manager.embedded_node().await {
                    let rpc_addr = format!("http://{}", node.config().rpc.http_addr);
                    self.call_external(&rpc_addr, "synor_estimateFee", ())
                        .await
                } else {
                    Err(crate::Error::NodeNotRunning)
                }
            }
        }
    }

    /// Gets block by hash
    pub async fn get_block(&self, hash: &str) -> crate::Result<serde_json::Value> {
        let mode = self.node_manager.connection_mode().await;

        match mode {
            ConnectionMode::Disconnected => Err(crate::Error::NotConnected),
            ConnectionMode::External { http_url, .. } => {
                self.call_external(&http_url, "synor_getBlock", (hash, true))
                    .await
            }
            #[cfg(feature = "embedded-node")]
            ConnectionMode::Embedded { .. } => {
                if let Some(node) = self.node_manager.embedded_node().await {
                    let rpc_addr = format!("http://{}", node.config().rpc.http_addr);
                    self.call_external(&rpc_addr, "synor_getBlock", (hash, true))
                        .await
                } else {
                    Err(crate::Error::NodeNotRunning)
                }
            }
        }
    }

    /// Gets current block template for mining
    pub async fn get_block_template(&self, coinbase_address: &str) -> crate::Result<serde_json::Value> {
        let mode = self.node_manager.connection_mode().await;

        match mode {
            ConnectionMode::Disconnected => Err(crate::Error::NotConnected),
            ConnectionMode::External { http_url, .. } => {
                self.call_external(&http_url, "synor_getBlockTemplate", (coinbase_address,))
                    .await
            }
            #[cfg(feature = "embedded-node")]
            ConnectionMode::Embedded { .. } => {
                if let Some(node) = self.node_manager.embedded_node().await {
                    let rpc_addr = format!("http://{}", node.config().rpc.http_addr);
                    self.call_external(&rpc_addr, "synor_getBlockTemplate", (coinbase_address,))
                        .await
                } else {
                    Err(crate::Error::NodeNotRunning)
                }
            }
        }
    }

    /// Submits a mined block
    pub async fn submit_block(&self, block: &serde_json::Value) -> crate::Result<bool> {
        let mode = self.node_manager.connection_mode().await;

        match mode {
            ConnectionMode::Disconnected => Err(crate::Error::NotConnected),
            ConnectionMode::External { http_url, .. } => {
                self.call_external(&http_url, "synor_submitBlock", (block,))
                    .await
            }
            #[cfg(feature = "embedded-node")]
            ConnectionMode::Embedded { .. } => {
                if let Some(node) = self.node_manager.embedded_node().await {
                    let rpc_addr = format!("http://{}", node.config().rpc.http_addr);
                    self.call_external(&rpc_addr, "synor_submitBlock", (block,))
                        .await
                } else {
                    Err(crate::Error::NodeNotRunning)
                }
            }
        }
    }

    /// Gets connected peers
    pub async fn get_peers(&self) -> crate::Result<Vec<crate::node::PeerInfo>> {
        let mode = self.node_manager.connection_mode().await;

        match mode {
            ConnectionMode::Disconnected => Err(crate::Error::NotConnected),
            ConnectionMode::External { http_url, .. } => {
                // External: call RPC
                let peers: Vec<serde_json::Value> = self
                    .call_external(&http_url, "net_getPeerInfo", ())
                    .await?;

                // Convert to our PeerInfo type
                let peer_infos = peers
                    .into_iter()
                    .map(|p| crate::node::PeerInfo {
                        peer_id: p["id"].as_str().unwrap_or("").to_string(),
                        address: p["address"].as_str().unwrap_or("").to_string(),
                        direction: if p["isOutbound"].as_bool().unwrap_or(false) {
                            "outbound"
                        } else {
                            "inbound"
                        }
                        .to_string(),
                        latency_ms: p["lastPingDuration"].as_u64(),
                        block_height: p["syncedHeaders"].as_u64().unwrap_or(0),
                        status: "connected".to_string(),
                    })
                    .collect();

                Ok(peer_infos)
            }
            #[cfg(feature = "embedded-node")]
            ConnectionMode::Embedded { .. } => {
                self.node_manager.get_peers().await
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_balance_serialization() {
        let balance = Balance {
            address: "synor:qztest123".to_string(),
            balance: 1_000_000_000,
        };

        let json = serde_json::to_string(&balance).unwrap();
        assert!(json.contains("synor:qztest123"));
        assert!(json.contains("1000000000"));
    }

    #[test]
    fn test_utxo_serialization() {
        let utxo = Utxo {
            transaction_id: "abc123".to_string(),
            index: 0,
            amount: 5_000_000_000,
            script_public_key: "76a914...88ac".to_string(),
            block_daa_score: 100,
            is_coinbase: false,
        };

        let json = serde_json::to_string(&utxo).unwrap();
        assert!(json.contains("transactionId"));
        assert!(json.contains("blockDaaScore"));
    }

    #[test]
    fn test_network_info_serialization() {
        let info = NetworkInfo {
            network: "testnet".to_string(),
            is_synced: true,
            block_height: 10000,
            blue_score: 5000,
            difficulty: 12345.67,
            peer_count: 25,
            mempool_size: 50,
        };

        let json = serde_json::to_string(&info).unwrap();
        assert!(json.contains("isSynced"));
        assert!(json.contains("blockHeight"));
    }
}
