//! Embedded Node Module
//!
//! Provides optional embedded Synor node functionality for the desktop wallet.
//! When enabled with the `embedded-node` feature, users can run a full node
//! directly inside the wallet application.

use std::path::PathBuf;
use std::sync::Arc;

use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use tracing::{error, info, warn};

/// Node connection mode - embedded node or external RPC
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ConnectionMode {
    /// No connection configured
    Disconnected,
    /// Connect to an external node via RPC
    External {
        http_url: String,
        ws_url: Option<String>,
    },
    /// Run embedded node (requires `embedded-node` feature)
    #[cfg(feature = "embedded-node")]
    Embedded {
        network: String,
        data_dir: Option<PathBuf>,
    },
}

impl Default for ConnectionMode {
    fn default() -> Self {
        ConnectionMode::Disconnected
    }
}

/// Node status information
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NodeStatus {
    /// Connection mode
    pub mode: ConnectionMode,
    /// Whether the node is connected/running
    pub is_connected: bool,
    /// Current block height
    pub block_height: u64,
    /// Current blue score (DAG metric)
    pub blue_score: u64,
    /// Number of connected peers
    pub peer_count: usize,
    /// Whether the node is syncing
    pub is_syncing: bool,
    /// Sync progress (0.0 - 1.0)
    pub sync_progress: f64,
    /// Network name
    pub network: String,
    /// Chain ID
    pub chain_id: u64,
}

impl Default for NodeStatus {
    fn default() -> Self {
        NodeStatus {
            mode: ConnectionMode::Disconnected,
            is_connected: false,
            block_height: 0,
            blue_score: 0,
            peer_count: 0,
            is_syncing: false,
            sync_progress: 0.0,
            network: String::new(),
            chain_id: 0,
        }
    }
}

/// Sync progress information
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SyncProgress {
    /// Current block height
    pub current_height: u64,
    /// Target block height (highest known)
    pub target_height: u64,
    /// Progress percentage (0.0 - 100.0)
    pub progress: f64,
    /// Estimated time remaining in seconds
    pub eta_seconds: Option<u64>,
    /// Sync status message
    pub status: String,
}

/// Peer information
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PeerInfo {
    /// Peer ID
    pub peer_id: String,
    /// Peer address
    pub address: String,
    /// Connection direction (inbound/outbound)
    pub direction: String,
    /// Latency in milliseconds
    pub latency_ms: Option<u64>,
    /// Peer's block height
    pub block_height: u64,
    /// Connection status
    pub status: String,
}

/// Manages node connection state and lifecycle
pub struct NodeManager {
    /// Current connection mode
    mode: RwLock<ConnectionMode>,
    /// Cached status
    status: RwLock<NodeStatus>,
    /// Embedded node instance (when feature enabled)
    #[cfg(feature = "embedded-node")]
    embedded_node: RwLock<Option<Arc<synord::SynorNode>>>,
    /// Tauri app handle for emitting events
    app_handle: Option<tauri::AppHandle>,
}

impl NodeManager {
    /// Creates a new node manager
    pub fn new() -> Self {
        NodeManager {
            mode: RwLock::new(ConnectionMode::Disconnected),
            status: RwLock::new(NodeStatus::default()),
            #[cfg(feature = "embedded-node")]
            embedded_node: RwLock::new(None),
            app_handle: None,
        }
    }

    /// Creates a new node manager with Tauri app handle for events
    pub fn with_app_handle(app_handle: tauri::AppHandle) -> Self {
        NodeManager {
            mode: RwLock::new(ConnectionMode::Disconnected),
            status: RwLock::new(NodeStatus::default()),
            #[cfg(feature = "embedded-node")]
            embedded_node: RwLock::new(None),
            app_handle: Some(app_handle),
        }
    }

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

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

    /// Connects to an external node via RPC
    pub async fn connect_external(
        &self,
        http_url: String,
        ws_url: Option<String>,
    ) -> crate::Result<()> {
        info!(http_url = %http_url, "Connecting to external node");

        // Update mode
        *self.mode.write().await = ConnectionMode::External {
            http_url: http_url.clone(),
            ws_url: ws_url.clone(),
        };

        // Update status
        let mut status = self.status.write().await;
        status.mode = ConnectionMode::External {
            http_url,
            ws_url,
        };
        status.is_connected = true;

        self.emit_status_changed(&status);
        Ok(())
    }

    /// Disconnects from the current node
    pub async fn disconnect(&self) -> crate::Result<()> {
        let current_mode = self.mode.read().await.clone();

        match current_mode {
            ConnectionMode::Disconnected => {
                // Already disconnected
                Ok(())
            }
            ConnectionMode::External { .. } => {
                info!("Disconnecting from external node");
                *self.mode.write().await = ConnectionMode::Disconnected;

                let mut status = self.status.write().await;
                *status = NodeStatus::default();
                self.emit_status_changed(&status);
                Ok(())
            }
            #[cfg(feature = "embedded-node")]
            ConnectionMode::Embedded { .. } => {
                self.stop_embedded_node().await
            }
        }
    }

    /// Emits a status changed event to the frontend
    fn emit_status_changed(&self, status: &NodeStatus) {
        if let Some(ref app) = self.app_handle {
            use tauri::Emitter;
            if let Err(e) = app.emit("node:status-changed", status) {
                warn!("Failed to emit node status: {}", e);
            }
        }
    }

    /// Emits a sync progress event to the frontend
    fn emit_sync_progress(&self, progress: &SyncProgress) {
        if let Some(ref app) = self.app_handle {
            use tauri::Emitter;
            if let Err(e) = app.emit("node:sync-progress", progress) {
                warn!("Failed to emit sync progress: {}", e);
            }
        }
    }
}

// ============================================================================
// Embedded Node Support (feature-gated)
// ============================================================================

#[cfg(feature = "embedded-node")]
impl NodeManager {
    /// Starts the embedded node
    pub async fn start_embedded_node(
        &self,
        network: &str,
        data_dir: Option<PathBuf>,
        mining_enabled: bool,
        coinbase_address: Option<String>,
        mining_threads: usize,
    ) -> crate::Result<()> {
        // Check if already running
        if self.embedded_node.read().await.is_some() {
            return Err(crate::Error::NodeAlreadyRunning);
        }

        info!(network = %network, "Starting embedded node");

        // Create node configuration
        let mut config = synord::NodeConfig::for_network(network)
            .map_err(|e| crate::Error::NodeError(e.to_string()))?;

        // Override data directory if specified
        if let Some(dir) = data_dir.clone() {
            config.data_dir = dir;
        }

        // Configure mining
        if mining_enabled {
            config.mining.enabled = true;
            config.mining.coinbase_address = coinbase_address;
            if mining_threads > 0 {
                config.mining.threads = mining_threads;
            }
        }

        // Configure RPC to use wallet-specific ports
        config.rpc.http_addr = "127.0.0.1:19423".to_string();
        config.rpc.ws_addr = "127.0.0.1:19424".to_string();

        // Configure P2P to use wallet-specific port
        config.p2p.listen_addr = format!("/ip4/0.0.0.0/tcp/{}", match network {
            "mainnet" => 19422,
            "testnet" => 19522,
            "devnet" => 19622,
            _ => 19422,
        });

        // Create and start the node
        let node = synord::SynorNode::new(config)
            .await
            .map_err(|e| crate::Error::NodeError(format!("Failed to create node: {}", e)))?;

        node.start()
            .await
            .map_err(|e| crate::Error::NodeError(format!("Failed to start node: {}", e)))?;

        let node = Arc::new(node);

        // Store the node
        *self.embedded_node.write().await = Some(node.clone());

        // Update mode and status
        *self.mode.write().await = ConnectionMode::Embedded {
            network: network.to_string(),
            data_dir,
        };

        let node_info = node.info().await;
        let mut status = self.status.write().await;
        status.mode = self.mode.read().await.clone();
        status.is_connected = true;
        status.network = node_info.network;
        status.chain_id = node_info.chain_id;
        status.block_height = node_info.block_height;
        status.blue_score = node_info.blue_score;
        status.peer_count = node_info.peer_count;
        status.is_syncing = node_info.is_syncing;

        self.emit_status_changed(&status);

        info!("Embedded node started successfully");
        Ok(())
    }

    /// Stops the embedded node
    pub async fn stop_embedded_node(&self) -> crate::Result<()> {
        let node = self.embedded_node.write().await.take();

        if let Some(node) = node {
            info!("Stopping embedded node");
            node.stop()
                .await
                .map_err(|e| crate::Error::NodeError(format!("Failed to stop node: {}", e)))?;
        }

        *self.mode.write().await = ConnectionMode::Disconnected;

        let mut status = self.status.write().await;
        *status = NodeStatus::default();
        self.emit_status_changed(&status);

        info!("Embedded node stopped");
        Ok(())
    }

    /// Gets the embedded node if running
    pub async fn embedded_node(&self) -> Option<Arc<synord::SynorNode>> {
        self.embedded_node.read().await.clone()
    }

    /// Refreshes the node status from the embedded node
    pub async fn refresh_status(&self) -> crate::Result<NodeStatus> {
        if let Some(node) = self.embedded_node.read().await.as_ref() {
            let node_info = node.info().await;
            let state = node.state().await;

            let mut status = self.status.write().await;
            status.block_height = node_info.block_height;
            status.blue_score = node_info.blue_score;
            status.peer_count = node_info.peer_count;
            status.is_syncing = node_info.is_syncing;

            // Calculate sync progress if syncing
            if node_info.is_syncing {
                // In a real implementation, we'd get the target height from peers
                status.sync_progress = 0.0; // Placeholder
            } else {
                status.sync_progress = 100.0;
            }

            self.emit_status_changed(&status);
            Ok(status.clone())
        } else {
            Ok(self.status.read().await.clone())
        }
    }

    /// Gets connected peers from the embedded node
    pub async fn get_peers(&self) -> crate::Result<Vec<PeerInfo>> {
        if let Some(node) = self.embedded_node.read().await.as_ref() {
            let peer_count = node.network().peer_count().await;
            // In a full implementation, we'd get detailed peer info from the network service
            // For now, return a placeholder
            Ok(vec![])
        } else {
            Ok(vec![])
        }
    }
}

// Stub implementations when embedded-node feature is disabled
#[cfg(not(feature = "embedded-node"))]
impl NodeManager {
    /// Stub: Embedded node not available
    pub async fn start_embedded_node(
        &self,
        _network: &str,
        _data_dir: Option<PathBuf>,
        _mining_enabled: bool,
        _coinbase_address: Option<String>,
        _mining_threads: usize,
    ) -> crate::Result<()> {
        Err(crate::Error::FeatureNotEnabled("embedded-node".to_string()))
    }

    /// Stub: Embedded node not available
    pub async fn stop_embedded_node(&self) -> crate::Result<()> {
        Err(crate::Error::FeatureNotEnabled("embedded-node".to_string()))
    }

    /// Stub: No embedded node
    pub async fn refresh_status(&self) -> crate::Result<NodeStatus> {
        Ok(self.status.read().await.clone())
    }

    /// Stub: No peers without embedded node
    pub async fn get_peers(&self) -> crate::Result<Vec<PeerInfo>> {
        Ok(vec![])
    }
}

impl Default for NodeManager {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn test_connection_mode_default() {
        let mode = ConnectionMode::default();
        assert_eq!(mode, ConnectionMode::Disconnected);
    }

    #[test]
    fn test_node_status_default() {
        let status = NodeStatus::default();
        assert!(!status.is_connected);
        assert_eq!(status.block_height, 0);
        assert_eq!(status.peer_count, 0);
    }

    #[tokio::test]
    async fn test_node_manager_creation() {
        let manager = NodeManager::new();
        let mode = manager.connection_mode().await;
        assert_eq!(mode, ConnectionMode::Disconnected);
    }

    #[tokio::test]
    async fn test_connect_external() {
        let manager = NodeManager::new();
        let result = manager
            .connect_external(
                "http://localhost:16110".to_string(),
                Some("ws://localhost:16111".to_string()),
            )
            .await;
        assert!(result.is_ok());

        let mode = manager.connection_mode().await;
        match mode {
            ConnectionMode::External { http_url, ws_url } => {
                assert_eq!(http_url, "http://localhost:16110");
                assert_eq!(ws_url, Some("ws://localhost:16111".to_string()));
            }
            _ => panic!("Expected External mode"),
        }
    }

    #[tokio::test]
    async fn test_disconnect() {
        let manager = NodeManager::new();
        manager
            .connect_external("http://localhost:16110".to_string(), None)
            .await
            .unwrap();

        let result = manager.disconnect().await;
        assert!(result.is_ok());

        let mode = manager.connection_mode().await;
        assert_eq!(mode, ConnectionMode::Disconnected);
    }
}
