//! Decentralized Oracle Network (Chainlink-style)
//!
//! Multiple independent oracle nodes submit prices with threshold signatures.
//! Aggregation happens on-chain with Byzantine fault tolerance.

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

/// Oracle node identity
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OracleNode {
    /// Unique node identifier
    pub node_id: String,
    /// Node's public key for signature verification
    pub public_key: Vec<u8>,
    /// Stake amount (for weighted voting)
    pub stake: SynorDecimal,
    /// Reputation score (0.0 - 1.0)
    pub reputation: f64,
    /// Whether node is currently active
    pub is_active: bool,
    /// Last heartbeat timestamp
    pub last_heartbeat: DateTime<Utc>,
}

impl OracleNode {
    pub fn new(node_id: impl Into<String>, public_key: Vec<u8>, stake: SynorDecimal) -> Self {
        Self {
            node_id: node_id.into(),
            public_key,
            stake,
            reputation: 1.0,
            is_active: true,
            last_heartbeat: Utc::now(),
        }
    }

    /// Check if node is eligible to submit prices
    pub fn is_eligible(&self, min_stake: SynorDecimal, min_reputation: f64) -> bool {
        self.is_active
            && self.stake >= min_stake
            && self.reputation >= min_reputation
            && !self.is_stale(Duration::minutes(5))
    }

    /// Check if node heartbeat is stale
    pub fn is_stale(&self, max_age: Duration) -> bool {
        Utc::now() - self.last_heartbeat > max_age
    }
}

/// Price submission from an oracle node
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PriceSubmission {
    /// Submitting node ID
    pub node_id: String,
    /// Token pair (e.g., "SYNOR/USD")
    pub pair: String,
    /// Submitted price
    pub price: SynorDecimal,
    /// Submission timestamp
    pub timestamp: DateTime<Utc>,
    /// Cryptographic signature
    pub signature: Vec<u8>,
    /// Round number for this aggregation
    pub round: u64,
}

/// Aggregation round for collecting submissions
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AggregationRound {
    /// Round number
    pub round: u64,
    /// Token pair
    pub pair: String,
    /// Start time
    pub started_at: DateTime<Utc>,
    /// Submission deadline
    pub deadline: DateTime<Utc>,
    /// Collected submissions
    pub submissions: Vec<PriceSubmission>,
    /// Whether round is finalized
    pub finalized: bool,
    /// Final aggregated price (if finalized)
    pub final_price: Option<SynorDecimal>,
}

impl AggregationRound {
    pub fn new(round: u64, pair: impl Into<String>, duration: Duration) -> Self {
        let now = Utc::now();
        Self {
            round,
            pair: pair.into(),
            started_at: now,
            deadline: now + duration,
            submissions: Vec::new(),
            finalized: false,
            final_price: None,
        }
    }

    /// Check if round is still accepting submissions
    pub fn is_open(&self) -> bool {
        !self.finalized && Utc::now() < self.deadline
    }

    /// Add a submission to this round
    pub fn add_submission(&mut self, submission: PriceSubmission) -> Result<()> {
        if self.finalized {
            return Err(EconomicsError::InvalidPrice("Round already finalized".into()));
        }
        if Utc::now() >= self.deadline {
            return Err(EconomicsError::InvalidPrice("Round deadline passed".into()));
        }
        if submission.round != self.round {
            return Err(EconomicsError::InvalidPrice("Wrong round number".into()));
        }

        // Check for duplicate submission from same node
        if self.submissions.iter().any(|s| s.node_id == submission.node_id) {
            return Err(EconomicsError::InvalidPrice("Duplicate submission".into()));
        }

        self.submissions.push(submission);
        Ok(())
    }
}

/// Configuration for decentralized oracle network
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecentralizedOracleConfig {
    /// Minimum number of submissions required
    pub min_submissions: usize,
    /// Maximum submissions to accept
    pub max_submissions: usize,
    /// Aggregation round duration
    pub round_duration: Duration,
    /// Minimum stake to participate
    pub min_stake: SynorDecimal,
    /// Minimum reputation to participate
    pub min_reputation: f64,
    /// Maximum price deviation from median (percentage)
    pub max_deviation: Decimal,
    /// Use stake-weighted aggregation
    pub stake_weighted: bool,
    /// Byzantine fault tolerance threshold (e.g., 0.33 for 1/3)
    pub bft_threshold: f64,
}

impl Default for DecentralizedOracleConfig {
    fn default() -> Self {
        Self {
            min_submissions: 3,
            max_submissions: 21,
            round_duration: Duration::seconds(30),
            min_stake: Decimal::new(1000, 0), // 1000 SYNOR minimum
            min_reputation: 0.5,
            max_deviation: Decimal::new(5, 2), // 5%
            stake_weighted: true,
            bft_threshold: 0.33,
        }
    }
}

/// Aggregation strategy for combining prices
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AggregationStrategy {
    /// Simple median (outlier resistant)
    Median,
    /// Stake-weighted median
    StakeWeightedMedian,
    /// Trimmed mean (remove top/bottom 10%)
    TrimmedMean,
    /// Reputation-weighted average
    ReputationWeighted,
}

/// Decentralized oracle network
pub struct DecentralizedOracle {
    config: DecentralizedOracleConfig,
    /// Registered oracle nodes
    nodes: HashMap<String, OracleNode>,
    /// Current aggregation rounds by pair
    current_rounds: HashMap<String, AggregationRound>,
    /// Historical rounds
    history: Vec<AggregationRound>,
    /// Current round number
    round_counter: u64,
    /// Aggregation strategy
    strategy: AggregationStrategy,
}

impl DecentralizedOracle {
    pub fn new() -> Self {
        Self::with_config(DecentralizedOracleConfig::default())
    }

    pub fn with_config(config: DecentralizedOracleConfig) -> Self {
        Self {
            config,
            nodes: HashMap::new(),
            current_rounds: HashMap::new(),
            history: Vec::new(),
            round_counter: 0,
            strategy: AggregationStrategy::StakeWeightedMedian,
        }
    }

    /// Register a new oracle node
    pub fn register_node(&mut self, node: OracleNode) -> Result<()> {
        if node.stake < self.config.min_stake {
            return Err(EconomicsError::InsufficientFunds {
                required: self.config.min_stake,
                available: node.stake,
            });
        }

        self.nodes.insert(node.node_id.clone(), node);
        Ok(())
    }

    /// Remove an oracle node
    pub fn remove_node(&mut self, node_id: &str) -> Option<OracleNode> {
        self.nodes.remove(node_id)
    }

    /// Update node heartbeat
    pub fn heartbeat(&mut self, node_id: &str) -> Result<()> {
        let node = self.nodes.get_mut(node_id)
            .ok_or_else(|| EconomicsError::InvalidPrice(format!("Unknown node: {}", node_id)))?;
        node.last_heartbeat = Utc::now();
        Ok(())
    }

    /// Start a new aggregation round for a pair
    pub fn start_round(&mut self, pair: impl Into<String>) -> u64 {
        let pair = pair.into();
        self.round_counter += 1;
        let round = AggregationRound::new(
            self.round_counter,
            pair.clone(),
            self.config.round_duration
        );
        self.current_rounds.insert(pair, round);
        self.round_counter
    }

    /// Submit a price for the current round
    pub fn submit_price(&mut self, submission: PriceSubmission) -> Result<()> {
        // Verify node exists and is eligible
        let node = self.nodes.get(&submission.node_id)
            .ok_or_else(|| EconomicsError::InvalidPrice("Unknown node".into()))?;

        if !node.is_eligible(self.config.min_stake, self.config.min_reputation) {
            return Err(EconomicsError::InvalidPrice("Node not eligible".into()));
        }

        // TODO: Verify signature using node.public_key
        // For now, we trust the submission

        // Add to current round
        let round = self.current_rounds.get_mut(&submission.pair)
            .ok_or_else(|| EconomicsError::InvalidPrice("No active round for pair".into()))?;

        round.add_submission(submission)
    }

    /// Finalize a round and aggregate prices
    pub fn finalize_round(&mut self, pair: &str) -> Result<SynorDecimal> {
        // First check state and get submissions (immutable borrow)
        let (is_finalized, existing_price, submissions) = {
            let round = self.current_rounds.get(pair)
                .ok_or_else(|| EconomicsError::PriceFeedUnavailable(pair.to_string()))?;

            if round.finalized {
                return round.final_price.ok_or_else(||
                    EconomicsError::InvalidPrice("Round has no price".into())
                );
            }

            // Check minimum submissions
            if round.submissions.len() < self.config.min_submissions {
                return Err(EconomicsError::InvalidPrice(format!(
                    "Insufficient submissions: {} < {}",
                    round.submissions.len(),
                    self.config.min_submissions
                )));
            }

            (round.finalized, round.final_price, round.submissions.clone())
        };

        if is_finalized {
            return existing_price.ok_or_else(||
                EconomicsError::InvalidPrice("Round has no price".into())
            );
        }

        // Filter outliers and aggregate (using cloned submissions)
        let final_price = self.aggregate_prices_from_vec(pair, &submissions)?;

        // Now get mutable reference to update
        if let Some(round) = self.current_rounds.get_mut(pair) {
            round.finalized = true;
            round.final_price = Some(final_price);
        }

        // Update node reputations based on accuracy
        self.update_reputations(pair, final_price);

        // Move to history
        if let Some(completed) = self.current_rounds.remove(pair) {
            self.history.push(completed);
            // Keep only last 1000 rounds
            if self.history.len() > 1000 {
                self.history.remove(0);
            }
        }

        Ok(final_price)
    }

    /// Aggregate prices from a vector of submissions (owned)
    fn aggregate_prices_from_vec(&self, pair: &str, submissions: &[PriceSubmission]) -> Result<SynorDecimal> {
        if submissions.is_empty() {
            return Err(EconomicsError::PriceFeedUnavailable(pair.to_string()));
        }

        // Filter outliers first
        let filtered = self.filter_outliers_vec(submissions);
        if filtered.is_empty() {
            return Err(EconomicsError::InvalidPrice("All submissions were outliers".into()));
        }

        let filtered_refs: Vec<_> = filtered.iter().collect();
        match self.strategy {
            AggregationStrategy::Median => self.calculate_median(&filtered_refs),
            AggregationStrategy::StakeWeightedMedian => self.calculate_stake_weighted_median(&filtered_refs),
            AggregationStrategy::TrimmedMean => self.calculate_trimmed_mean(&filtered_refs),
            AggregationStrategy::ReputationWeighted => self.calculate_reputation_weighted(&filtered_refs),
        }
    }

    /// Filter out outlier submissions (returns owned vector)
    fn filter_outliers_vec(&self, submissions: &[PriceSubmission]) -> Vec<PriceSubmission> {
        if submissions.len() < 3 {
            return submissions.to_vec();
        }

        // Calculate median
        let mut prices: Vec<_> = submissions.iter().map(|s| s.price).collect();
        prices.sort();
        let median = prices[prices.len() / 2];

        // Filter submissions within max_deviation of median
        submissions
            .iter()
            .filter(|s| {
                let deviation = (s.price - median).abs() / median;
                deviation <= self.config.max_deviation
            })
            .cloned()
            .collect()
    }

    /// Calculate simple median
    fn calculate_median(&self, submissions: &[&PriceSubmission]) -> Result<SynorDecimal> {
        let mut prices: Vec<_> = submissions.iter().map(|s| s.price).collect();
        prices.sort();
        Ok(prices[prices.len() / 2])
    }

    /// Calculate stake-weighted median
    fn calculate_stake_weighted_median(&self, submissions: &[&PriceSubmission]) -> Result<SynorDecimal> {
        // Get stake for each submission
        let mut weighted: Vec<(SynorDecimal, SynorDecimal)> = submissions
            .iter()
            .filter_map(|s| {
                self.nodes.get(&s.node_id).map(|n| (s.price, n.stake))
            })
            .collect();

        if weighted.is_empty() {
            return self.calculate_median(submissions);
        }

        // Sort by price
        weighted.sort_by(|a, b| a.0.cmp(&b.0));

        // Find weighted median
        let total_stake: SynorDecimal = weighted.iter().map(|(_, s)| *s).sum();
        let half_stake = total_stake / Decimal::from(2);

        let mut cumulative = Decimal::ZERO;
        for (price, stake) in &weighted {
            cumulative += *stake;
            if cumulative >= half_stake {
                return Ok(*price);
            }
        }

        Ok(weighted.last().map(|(p, _)| *p).unwrap_or(Decimal::ZERO))
    }

    /// Calculate trimmed mean (remove top/bottom 10%)
    fn calculate_trimmed_mean(&self, submissions: &[&PriceSubmission]) -> Result<SynorDecimal> {
        let mut prices: Vec<_> = submissions.iter().map(|s| s.price).collect();
        prices.sort();

        let trim_count = (prices.len() as f64 * 0.1).ceil() as usize;
        let trimmed = &prices[trim_count..prices.len() - trim_count];

        if trimmed.is_empty() {
            return self.calculate_median(submissions);
        }

        let sum: SynorDecimal = trimmed.iter().copied().sum();
        Ok(sum / Decimal::from(trimmed.len()))
    }

    /// Calculate reputation-weighted average
    fn calculate_reputation_weighted(&self, submissions: &[&PriceSubmission]) -> Result<SynorDecimal> {
        let mut weighted_sum = Decimal::ZERO;
        let mut total_weight = Decimal::ZERO;

        for sub in submissions {
            let reputation = self.nodes.get(&sub.node_id)
                .map(|n| n.reputation)
                .unwrap_or(0.5);

            let weight = Decimal::from_f64_retain(reputation).unwrap_or(Decimal::ONE);
            weighted_sum += sub.price * weight;
            total_weight += weight;
        }

        if total_weight == Decimal::ZERO {
            return self.calculate_median(submissions);
        }

        Ok(weighted_sum / total_weight)
    }

    /// Update node reputations based on submission accuracy
    fn update_reputations(&mut self, _pair: &str, final_price: SynorDecimal) {
        // Get submissions from current round before it was moved
        let submissions: Vec<_> = self.history.last()
            .map(|r| r.submissions.clone())
            .unwrap_or_default();

        for sub in submissions {
            if let Some(node) = self.nodes.get_mut(&sub.node_id) {
                let deviation = (sub.price - final_price).abs() / final_price;

                // Increase reputation for accurate submissions, decrease for inaccurate
                if deviation <= Decimal::new(1, 2) { // Within 1%
                    node.reputation = (node.reputation + 0.01).min(1.0);
                } else if deviation > self.config.max_deviation {
                    node.reputation = (node.reputation - 0.05).max(0.0);
                }
            }
        }
    }

    /// Get current round status
    pub fn get_round_status(&self, pair: &str) -> Option<&AggregationRound> {
        self.current_rounds.get(pair)
    }

    /// Get number of active nodes
    pub fn active_node_count(&self) -> usize {
        self.nodes.values()
            .filter(|n| n.is_eligible(self.config.min_stake, self.config.min_reputation))
            .count()
    }

    /// Check if network has sufficient nodes for BFT
    pub fn has_quorum(&self) -> bool {
        let active = self.active_node_count();
        let required = (active as f64 * (1.0 - self.config.bft_threshold)).ceil() as usize;
        active >= self.config.min_submissions && required >= 2
    }

    /// Get all registered nodes
    pub fn get_nodes(&self) -> Vec<&OracleNode> {
        self.nodes.values().collect()
    }

    /// Convert finalized price to TokenPrice
    pub fn to_token_price(&self, pair: &str) -> Option<TokenPrice> {
        self.history.iter()
            .rev()
            .find(|r| r.pair == pair && r.finalized)
            .and_then(|r| r.final_price.map(|price| {
                let parts: Vec<_> = pair.split('/').collect();
                TokenPrice {
                    token: parts.get(0).unwrap_or(&"").to_string(),
                    quote: parts.get(1).unwrap_or(&"").to_string(),
                    price,
                    timestamp: r.deadline,
                    source: PriceSource::Aggregated,
                    confidence: 1.0,
                }
            }))
    }
}

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

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

    fn create_test_nodes() -> Vec<OracleNode> {
        (0..5).map(|i| {
            OracleNode::new(
                format!("node_{}", i),
                vec![i as u8; 32],
                dec!(10000), // 10k stake
            )
        }).collect()
    }

    #[test]
    fn test_register_nodes() {
        let mut oracle = DecentralizedOracle::new();

        for node in create_test_nodes() {
            oracle.register_node(node).unwrap();
        }

        assert_eq!(oracle.active_node_count(), 5);
        assert!(oracle.has_quorum());
    }

    #[test]
    fn test_aggregation_round() {
        let mut oracle = DecentralizedOracle::new();

        for node in create_test_nodes() {
            oracle.register_node(node).unwrap();
        }

        // Start round
        let round = oracle.start_round("SYNOR/USD");
        assert_eq!(round, 1);

        // Submit prices
        let prices = [dec!(1.00), dec!(1.01), dec!(1.02), dec!(0.99), dec!(1.00)];
        for (i, price) in prices.iter().enumerate() {
            let submission = PriceSubmission {
                node_id: format!("node_{}", i),
                pair: "SYNOR/USD".to_string(),
                price: *price,
                timestamp: Utc::now(),
                signature: vec![],
                round: 1,
            };
            oracle.submit_price(submission).unwrap();
        }

        // Finalize
        let final_price = oracle.finalize_round("SYNOR/USD").unwrap();

        // Should be close to median (~1.00)
        assert!(final_price >= dec!(0.99) && final_price <= dec!(1.02));
    }

    #[test]
    fn test_outlier_filtering() {
        let mut oracle = DecentralizedOracle::new();

        for node in create_test_nodes() {
            oracle.register_node(node).unwrap();
        }

        oracle.start_round("SYNOR/USD");

        // Submit with one outlier
        let prices = [dec!(1.00), dec!(1.01), dec!(1.02), dec!(1.00), dec!(5.00)]; // 5.00 is outlier
        for (i, price) in prices.iter().enumerate() {
            let submission = PriceSubmission {
                node_id: format!("node_{}", i),
                pair: "SYNOR/USD".to_string(),
                price: *price,
                timestamp: Utc::now(),
                signature: vec![],
                round: 1,
            };
            oracle.submit_price(submission).unwrap();
        }

        let final_price = oracle.finalize_round("SYNOR/USD").unwrap();

        // Outlier should be filtered, price should be ~1.00-1.02
        assert!(final_price >= dec!(0.99) && final_price <= dec!(1.03));
    }

    #[test]
    fn test_insufficient_stake() {
        let mut oracle = DecentralizedOracle::new();

        let low_stake_node = OracleNode::new("poor_node", vec![0; 32], dec!(100)); // Below min
        let result = oracle.register_node(low_stake_node);

        assert!(result.is_err());
    }
}
