//! Cross-Chain Price Feeds via IBC
//!
//! Pull ETH/BTC/stablecoin prices from Ethereum, Cosmos, and other chains
//! via Inter-Blockchain Communication protocol.

use crate::error::{EconomicsError, Result};
use crate::oracle::{PriceSource, TokenPrice};
use crate::SynorDecimal;
use async_trait::async_trait;
use chrono::{DateTime, Duration, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Supported cross-chain networks
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ChainNetwork {
    /// Ethereum mainnet
    Ethereum,
    /// Binance Smart Chain
    BinanceSmartChain,
    /// Polygon
    Polygon,
    /// Arbitrum
    Arbitrum,
    /// Optimism
    Optimism,
    /// Cosmos Hub
    Cosmos,
    /// Osmosis DEX
    Osmosis,
    /// Avalanche
    Avalanche,
    /// Solana
    Solana,
    /// Bitcoin (via bridge)
    Bitcoin,
}

impl ChainNetwork {
    /// Get IBC channel ID for this network
    pub fn ibc_channel(&self) -> Option<&str> {
        match self {
            ChainNetwork::Cosmos => Some("channel-0"),
            ChainNetwork::Osmosis => Some("channel-1"),
            _ => None, // Non-IBC chains use bridges
        }
    }

    /// Get native token symbol
    pub fn native_token(&self) -> &str {
        match self {
            ChainNetwork::Ethereum => "ETH",
            ChainNetwork::BinanceSmartChain => "BNB",
            ChainNetwork::Polygon => "MATIC",
            ChainNetwork::Arbitrum => "ETH",
            ChainNetwork::Optimism => "ETH",
            ChainNetwork::Cosmos => "ATOM",
            ChainNetwork::Osmosis => "OSMO",
            ChainNetwork::Avalanche => "AVAX",
            ChainNetwork::Solana => "SOL",
            ChainNetwork::Bitcoin => "BTC",
        }
    }

    /// Check if this is a Cosmos SDK chain (native IBC)
    pub fn is_ibc_native(&self) -> bool {
        matches!(self, ChainNetwork::Cosmos | ChainNetwork::Osmosis)
    }
}

/// Cross-chain price packet (IBC format)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrossChainPricePacket {
    /// Source chain
    pub source_chain: ChainNetwork,
    /// Token symbol
    pub token: String,
    /// Quote currency
    pub quote: String,
    /// Price value
    pub price: SynorDecimal,
    /// Block height at source
    pub source_block: u64,
    /// Timestamp at source
    pub source_timestamp: DateTime<Utc>,
    /// Merkle proof for verification
    pub proof: Option<MerkleProof>,
    /// Oracle signatures (for bridge chains)
    pub signatures: Vec<OracleSignature>,
}

/// Merkle proof for IBC verification
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MerkleProof {
    /// Proof path
    pub path: Vec<ProofNode>,
    /// Root hash
    pub root: [u8; 32],
    /// Leaf hash
    pub leaf: [u8; 32],
}

/// Node in Merkle proof path
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProofNode {
    pub hash: [u8; 32],
    pub is_left: bool,
}

/// Oracle signature for bridge verification
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OracleSignature {
    pub signer: String,
    pub signature: Vec<u8>,
    pub timestamp: DateTime<Utc>,
}

impl CrossChainPricePacket {
    /// Verify the packet using Merkle proof (for IBC)
    pub fn verify_merkle_proof(&self) -> bool {
        match &self.proof {
            Some(proof) => {
                let mut current = proof.leaf;
                for node in &proof.path {
                    current = if node.is_left {
                        hash_pair(&node.hash, &current)
                    } else {
                        hash_pair(&current, &node.hash)
                    };
                }
                current == proof.root
            }
            None => false,
        }
    }

    /// Verify bridge oracle signatures (threshold)
    pub fn verify_signatures(&self, threshold: usize) -> bool {
        // In production, verify each signature cryptographically
        // For now, just check we have enough
        self.signatures.len() >= threshold
    }

    /// Convert to TokenPrice
    pub fn to_token_price(&self) -> TokenPrice {
        TokenPrice {
            token: self.token.clone(),
            quote: self.quote.clone(),
            price: self.price,
            timestamp: self.source_timestamp,
            source: PriceSource::CrossChain,
            confidence: self.calculate_confidence(),
        }
    }

    fn calculate_confidence(&self) -> f64 {
        let age = (Utc::now() - self.source_timestamp).num_seconds() as f64;
        let freshness = (1.0 - age / 300.0).max(0.0); // 5 min staleness

        let has_proof = if self.proof.is_some() { 1.0 } else { 0.0 };
        let sig_count = (self.signatures.len() as f64 / 3.0).min(1.0);

        (freshness * 0.4 + has_proof * 0.3 + sig_count * 0.3).min(1.0)
    }
}

/// Simple hash function for Merkle proofs
fn hash_pair(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};

    let mut hasher = DefaultHasher::new();
    left.hash(&mut hasher);
    right.hash(&mut hasher);
    let hash = hasher.finish();

    let mut result = [0u8; 32];
    result[..8].copy_from_slice(&hash.to_le_bytes());
    result
}

/// Configuration for cross-chain feeds
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrossChainConfig {
    /// Enabled chains
    pub enabled_chains: Vec<ChainNetwork>,
    /// Minimum signatures required for bridge verification
    pub min_signatures: usize,
    /// Maximum packet age (seconds)
    pub max_packet_age: i64,
    /// Update interval (seconds)
    pub update_interval: i64,
    /// IBC timeout (seconds)
    pub ibc_timeout: i64,
    /// Tokens to track per chain
    pub tracked_tokens: HashMap<ChainNetwork, Vec<String>>,
}

impl Default for CrossChainConfig {
    fn default() -> Self {
        let mut tracked = HashMap::new();
        tracked.insert(ChainNetwork::Ethereum, vec!["ETH".to_string(), "USDC".to_string(), "USDT".to_string()]);
        tracked.insert(ChainNetwork::Bitcoin, vec!["BTC".to_string()]);
        tracked.insert(ChainNetwork::Cosmos, vec!["ATOM".to_string()]);
        tracked.insert(ChainNetwork::Osmosis, vec!["OSMO".to_string()]);

        Self {
            enabled_chains: vec![
                ChainNetwork::Ethereum,
                ChainNetwork::Bitcoin,
                ChainNetwork::Cosmos,
                ChainNetwork::Osmosis,
            ],
            min_signatures: 3,
            max_packet_age: 300, // 5 minutes
            update_interval: 30,
            ibc_timeout: 600, // 10 minutes
            tracked_tokens: tracked,
        }
    }
}

/// Interface for chain-specific price fetchers
#[async_trait]
pub trait ChainPriceFetcher: Send + Sync {
    /// Get the chain this fetcher supports
    fn chain(&self) -> ChainNetwork;

    /// Fetch current price for a token
    async fn fetch_price(&self, token: &str, quote: &str) -> Result<CrossChainPricePacket>;

    /// Verify a received packet
    fn verify_packet(&self, packet: &CrossChainPricePacket) -> bool;

    /// Get supported tokens
    fn supported_tokens(&self) -> Vec<String>;
}

/// Cross-chain price feed manager
pub struct CrossChainOracle {
    config: CrossChainConfig,
    /// Fetchers by chain
    fetchers: HashMap<ChainNetwork, Box<dyn ChainPriceFetcher>>,
    /// Cached prices by pair
    cache: HashMap<String, CrossChainPrice>,
    /// Pending IBC packets
    pending_packets: Vec<PendingPacket>,
}

/// Cached cross-chain price
#[derive(Debug, Clone)]
struct CrossChainPrice {
    packet: CrossChainPricePacket,
    received_at: DateTime<Utc>,
    verified: bool,
}

/// Pending IBC packet awaiting confirmation
#[derive(Debug, Clone)]
pub struct PendingPacket {
    /// The price packet being sent
    pub packet: CrossChainPricePacket,
    /// When the packet was sent
    pub sent_at: DateTime<Utc>,
    /// IBC channel
    pub channel: String,
    /// Sequence number
    pub sequence: u64,
}

impl CrossChainOracle {
    pub fn new() -> Self {
        Self::with_config(CrossChainConfig::default())
    }

    pub fn with_config(config: CrossChainConfig) -> Self {
        Self {
            config,
            fetchers: HashMap::new(),
            cache: HashMap::new(),
            pending_packets: Vec::new(),
        }
    }

    /// Register a chain price fetcher
    pub fn register_fetcher(&mut self, fetcher: Box<dyn ChainPriceFetcher>) {
        let chain = fetcher.chain();
        self.fetchers.insert(chain, fetcher);
    }

    /// Receive and process an IBC price packet
    pub fn receive_ibc_packet(&mut self, packet: CrossChainPricePacket) -> Result<()> {
        // Check age
        let age = (Utc::now() - packet.source_timestamp).num_seconds();
        if age > self.config.max_packet_age {
            return Err(EconomicsError::InvalidPrice("Packet too old".into()));
        }

        // Verify based on source
        let verified = if packet.source_chain.is_ibc_native() {
            packet.verify_merkle_proof()
        } else {
            packet.verify_signatures(self.config.min_signatures)
        };

        if !verified {
            return Err(EconomicsError::InvalidPrice("Packet verification failed".into()));
        }

        // Cache the price
        let pair_key = format!("{}/{}", packet.token, packet.quote);
        self.cache.insert(pair_key, CrossChainPrice {
            packet,
            received_at: Utc::now(),
            verified,
        });

        Ok(())
    }

    /// Fetch price from a specific chain
    pub async fn fetch_from_chain(
        &mut self,
        chain: ChainNetwork,
        token: &str,
        quote: &str,
    ) -> Result<CrossChainPricePacket> {
        let fetcher = self.fetchers.get(&chain)
            .ok_or_else(|| EconomicsError::PriceFeedUnavailable(format!("{:?}", chain)))?;

        let packet = fetcher.fetch_price(token, quote).await?;

        // Verify and cache
        if fetcher.verify_packet(&packet) {
            let pair_key = format!("{}/{}", token, quote);
            self.cache.insert(pair_key.clone(), CrossChainPrice {
                packet: packet.clone(),
                received_at: Utc::now(),
                verified: true,
            });
        }

        Ok(packet)
    }

    /// Get cached price for a token pair
    pub fn get_price(&self, token: &str, quote: &str) -> Option<SynorDecimal> {
        let pair_key = format!("{}/{}", token, quote);
        self.cache.get(&pair_key)
            .filter(|c| c.verified)
            .filter(|c| (Utc::now() - c.received_at).num_seconds() < self.config.max_packet_age)
            .map(|c| c.packet.price)
    }

    /// Get price with full packet info
    pub fn get_price_with_info(&self, token: &str, quote: &str) -> Option<&CrossChainPricePacket> {
        let pair_key = format!("{}/{}", token, quote);
        self.cache.get(&pair_key)
            .filter(|c| c.verified)
            .map(|c| &c.packet)
    }

    /// Fetch all tracked prices from all chains
    pub async fn fetch_all(&mut self) -> Result<Vec<CrossChainPricePacket>> {
        let mut results = Vec::new();

        for (chain, tokens) in &self.config.tracked_tokens.clone() {
            if !self.config.enabled_chains.contains(chain) {
                continue;
            }

            for token in tokens {
                match self.fetch_from_chain(*chain, token, "USD").await {
                    Ok(packet) => results.push(packet),
                    Err(e) => {
                        tracing::warn!("Failed to fetch {:?} {} price: {}", chain, token, e);
                    }
                }
            }
        }

        Ok(results)
    }

    /// Create an IBC price request packet
    pub fn create_ibc_request(
        &self,
        target_chain: ChainNetwork,
        token: &str,
        quote: &str,
    ) -> Option<IBCPriceRequest> {
        let channel = target_chain.ibc_channel()?;

        Some(IBCPriceRequest {
            target_chain,
            channel: channel.to_string(),
            token: token.to_string(),
            quote: quote.to_string(),
            timeout: Utc::now() + Duration::seconds(self.config.ibc_timeout),
        })
    }

    /// Get supported chains
    pub fn supported_chains(&self) -> &[ChainNetwork] {
        &self.config.enabled_chains
    }

    /// Get all cached prices
    pub fn get_all_prices(&self) -> Vec<TokenPrice> {
        self.cache.values()
            .filter(|c| c.verified)
            .map(|c| c.packet.to_token_price())
            .collect()
    }

    /// Clear stale cache entries
    pub fn cleanup_cache(&mut self) {
        let max_age = self.config.max_packet_age;
        self.cache.retain(|_, v| {
            (Utc::now() - v.received_at).num_seconds() < max_age
        });
    }

    /// Send an IBC price request and track pending packet
    pub fn send_ibc_request(
        &mut self,
        request: IBCPriceRequest,
        sequence: u64,
    ) -> Option<PendingPacket> {
        // Create a placeholder packet for the request
        let pending = PendingPacket {
            packet: CrossChainPricePacket {
                source_chain: request.target_chain,
                token: request.token.clone(),
                quote: request.quote.clone(),
                price: Decimal::ZERO, // Will be filled on response
                source_block: 0,
                source_timestamp: Utc::now(),
                proof: None,
                signatures: vec![],
            },
            sent_at: Utc::now(),
            channel: request.channel.clone(),
            sequence,
        };

        self.pending_packets.push(pending.clone());
        Some(pending)
    }

    /// Confirm a pending packet was received
    pub fn confirm_pending_packet(&mut self, channel: &str, sequence: u64) -> Option<PendingPacket> {
        if let Some(idx) = self
            .pending_packets
            .iter()
            .position(|p| p.channel == channel && p.sequence == sequence)
        {
            Some(self.pending_packets.remove(idx))
        } else {
            None
        }
    }

    /// Get pending packets count
    pub fn pending_count(&self) -> usize {
        self.pending_packets.len()
    }

    /// Cleanup timed out pending packets
    pub fn cleanup_pending(&mut self, timeout_secs: i64) {
        self.pending_packets.retain(|p| {
            (Utc::now() - p.sent_at).num_seconds() < timeout_secs
        });
    }
}

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

/// IBC price request packet
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IBCPriceRequest {
    pub target_chain: ChainNetwork,
    pub channel: String,
    pub token: String,
    pub quote: String,
    pub timeout: DateTime<Utc>,
}

/// Ethereum price fetcher using Chainlink or Uniswap
pub struct EthereumPriceFetcher {
    /// RPC endpoint
    rpc_url: String,
    /// Chainlink aggregator addresses
    chainlink_feeds: HashMap<String, String>,
}

impl EthereumPriceFetcher {
    pub fn new(rpc_url: impl Into<String>) -> Self {
        let mut feeds = HashMap::new();
        feeds.insert("ETH/USD".to_string(), "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419".to_string());
        feeds.insert("BTC/USD".to_string(), "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c".to_string());
        feeds.insert("USDC/USD".to_string(), "0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6".to_string());

        Self {
            rpc_url: rpc_url.into(),
            chainlink_feeds: feeds,
        }
    }

    /// Get the RPC endpoint URL
    pub fn rpc_url(&self) -> &str {
        &self.rpc_url
    }
}

#[async_trait]
impl ChainPriceFetcher for EthereumPriceFetcher {
    fn chain(&self) -> ChainNetwork {
        ChainNetwork::Ethereum
    }

    async fn fetch_price(&self, token: &str, quote: &str) -> Result<CrossChainPricePacket> {
        let pair = format!("{}/{}", token, quote);
        let _feed_addr = self.chainlink_feeds.get(&pair)
            .ok_or_else(|| EconomicsError::PriceFeedUnavailable(pair.clone()))?;

        // In production: Call Chainlink aggregator via ethers-rs
        // For now, return mock data
        Ok(CrossChainPricePacket {
            source_chain: ChainNetwork::Ethereum,
            token: token.to_string(),
            quote: quote.to_string(),
            price: Decimal::new(200000, 2), // Mock: $2000 for ETH
            source_block: 19000000,
            source_timestamp: Utc::now(),
            proof: None,
            signatures: vec![
                OracleSignature {
                    signer: "chainlink".to_string(),
                    signature: vec![0; 65],
                    timestamp: Utc::now(),
                },
            ],
        })
    }

    fn verify_packet(&self, packet: &CrossChainPricePacket) -> bool {
        packet.source_chain == ChainNetwork::Ethereum
            && !packet.signatures.is_empty()
    }

    fn supported_tokens(&self) -> Vec<String> {
        self.chainlink_feeds.keys().cloned().collect()
    }
}

/// Cosmos/IBC price fetcher
pub struct CosmosPriceFetcher {
    /// Light client connection
    light_client_id: String,
    /// Chain ID
    chain_id: String,
}

impl CosmosPriceFetcher {
    pub fn new(light_client_id: impl Into<String>, chain_id: impl Into<String>) -> Self {
        Self {
            light_client_id: light_client_id.into(),
            chain_id: chain_id.into(),
        }
    }

    /// Get the light client ID
    pub fn light_client_id(&self) -> &str {
        &self.light_client_id
    }

    /// Get the chain ID
    pub fn chain_id(&self) -> &str {
        &self.chain_id
    }
}

#[async_trait]
impl ChainPriceFetcher for CosmosPriceFetcher {
    fn chain(&self) -> ChainNetwork {
        ChainNetwork::Cosmos
    }

    async fn fetch_price(&self, token: &str, quote: &str) -> Result<CrossChainPricePacket> {
        // In production: Query via IBC light client
        Ok(CrossChainPricePacket {
            source_chain: ChainNetwork::Cosmos,
            token: token.to_string(),
            quote: quote.to_string(),
            price: Decimal::new(1000, 2), // Mock: $10 for ATOM
            source_block: 15000000,
            source_timestamp: Utc::now(),
            proof: Some(MerkleProof {
                path: vec![],
                root: [0; 32],
                leaf: [0; 32],
            }),
            signatures: vec![],
        })
    }

    fn verify_packet(&self, packet: &CrossChainPricePacket) -> bool {
        packet.source_chain == ChainNetwork::Cosmos
            && packet.proof.is_some()
    }

    fn supported_tokens(&self) -> Vec<String> {
        vec!["ATOM/USD".to_string()]
    }
}

/// Builder for creating a fully-configured CrossChainOracle
pub struct CrossChainOracleBuilder {
    config: CrossChainConfig,
    ethereum_rpc: Option<String>,
    cosmos_light_client: Option<(String, String)>,
}

impl CrossChainOracleBuilder {
    /// Create a new builder with default config
    pub fn new() -> Self {
        Self {
            config: CrossChainConfig::default(),
            ethereum_rpc: None,
            cosmos_light_client: None,
        }
    }

    /// Set custom configuration
    pub fn with_config(mut self, config: CrossChainConfig) -> Self {
        self.config = config;
        self
    }

    /// Add Ethereum price fetcher (Chainlink integration)
    pub fn with_ethereum(mut self, rpc_url: impl Into<String>) -> Self {
        self.ethereum_rpc = Some(rpc_url.into());
        self
    }

    /// Add Cosmos/IBC price fetcher
    pub fn with_cosmos(mut self, light_client_id: impl Into<String>, chain_id: impl Into<String>) -> Self {
        self.cosmos_light_client = Some((light_client_id.into(), chain_id.into()));
        self
    }

    /// Build the configured CrossChainOracle
    pub fn build(self) -> CrossChainOracle {
        let mut oracle = CrossChainOracle::with_config(self.config);

        // Register Ethereum fetcher if configured
        if let Some(rpc_url) = self.ethereum_rpc {
            oracle.register_fetcher(Box::new(EthereumPriceFetcher::new(rpc_url)));
        }

        // Register Cosmos fetcher if configured
        if let Some((light_client_id, chain_id)) = self.cosmos_light_client {
            oracle.register_fetcher(Box::new(CosmosPriceFetcher::new(light_client_id, chain_id)));
        }

        oracle
    }
}

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

/// Factory for creating pre-configured cross-chain oracles
pub struct CrossChainOracleFactory;

impl CrossChainOracleFactory {
    /// Create a development oracle with mock fetchers
    pub fn development() -> CrossChainOracle {
        CrossChainOracleBuilder::new()
            .with_ethereum("http://localhost:8545")
            .with_cosmos("07-tendermint-0", "cosmoshub-4")
            .build()
    }

    /// Create a production oracle with real endpoints
    pub fn production(config: CrossChainProductionConfig) -> CrossChainOracle {
        let mut builder = CrossChainOracleBuilder::new()
            .with_config(config.cross_chain_config);

        if let Some(eth_rpc) = config.ethereum_rpc_url {
            builder = builder.with_ethereum(eth_rpc);
        }

        if let Some((light_client, chain_id)) = config.cosmos_light_client {
            builder = builder.with_cosmos(light_client, chain_id);
        }

        builder.build()
    }

    /// Create an oracle with only Ethereum support
    pub fn ethereum_only(rpc_url: impl Into<String>) -> CrossChainOracle {
        CrossChainOracleBuilder::new()
            .with_ethereum(rpc_url)
            .build()
    }

    /// Create an oracle with only Cosmos/IBC support
    pub fn cosmos_only(light_client_id: impl Into<String>, chain_id: impl Into<String>) -> CrossChainOracle {
        CrossChainOracleBuilder::new()
            .with_cosmos(light_client_id, chain_id)
            .build()
    }
}

/// Configuration for production cross-chain oracle
#[derive(Debug, Clone, Default)]
pub struct CrossChainProductionConfig {
    /// Base cross-chain configuration
    pub cross_chain_config: CrossChainConfig,
    /// Ethereum RPC URL (e.g., Infura/Alchemy)
    pub ethereum_rpc_url: Option<String>,
    /// Cosmos light client ID and chain ID
    pub cosmos_light_client: Option<(String, String)>,
}

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

    #[test]
    fn test_chain_network() {
        assert_eq!(ChainNetwork::Ethereum.native_token(), "ETH");
        assert!(ChainNetwork::Cosmos.is_ibc_native());
        assert!(!ChainNetwork::Ethereum.is_ibc_native());
    }

    #[test]
    fn test_packet_confidence() {
        let packet = CrossChainPricePacket {
            source_chain: ChainNetwork::Ethereum,
            token: "ETH".to_string(),
            quote: "USD".to_string(),
            price: dec!(2000),
            source_block: 19000000,
            source_timestamp: Utc::now(),
            proof: Some(MerkleProof {
                path: vec![],
                root: [0; 32],
                leaf: [0; 32],
            }),
            signatures: vec![
                OracleSignature {
                    signer: "a".to_string(),
                    signature: vec![],
                    timestamp: Utc::now(),
                },
                OracleSignature {
                    signer: "b".to_string(),
                    signature: vec![],
                    timestamp: Utc::now(),
                },
                OracleSignature {
                    signer: "c".to_string(),
                    signature: vec![],
                    timestamp: Utc::now(),
                },
            ],
        };

        let confidence = packet.calculate_confidence();
        assert!(confidence > 0.5);
    }

    #[test]
    fn test_cross_chain_oracle() {
        let mut oracle = CrossChainOracle::new();

        let packet = CrossChainPricePacket {
            source_chain: ChainNetwork::Cosmos,
            token: "ATOM".to_string(),
            quote: "USD".to_string(),
            price: dec!(10.50),
            source_block: 15000000,
            source_timestamp: Utc::now(),
            proof: Some(MerkleProof {
                path: vec![],
                root: [0; 32],
                leaf: [0; 32],
            }),
            signatures: vec![],
        };

        oracle.receive_ibc_packet(packet).unwrap();

        let price = oracle.get_price("ATOM", "USD");
        assert_eq!(price, Some(dec!(10.50)));
    }
}
