//! Difficulty adjustment algorithm (DAA) for Synor.
//!
//! Synor uses a DAA similar to Kaspa, which adjusts difficulty every block
//! based on a weighted moving average of recent block times.
//!
//! Target: 10 blocks per second (100ms block time)

use synor_types::Hash256;
use thiserror::Error;

/// DAA parameters.
#[derive(Clone, Debug)]
pub struct DaaParams {
    /// Target time between blocks (milliseconds).
    pub target_time_ms: u64,
    /// Number of blocks in the DAA window.
    pub window_size: u64,
    /// Maximum difficulty adjustment factor per block.
    pub max_adjustment_factor: f64,
    /// Minimum difficulty (for testing).
    pub min_difficulty: u64,
}

impl Default for DaaParams {
    fn default() -> Self {
        DaaParams {
            target_time_ms: crate::TARGET_BLOCK_TIME_MS,
            window_size: 2641, // ~264 seconds of blocks at 10 bps
            max_adjustment_factor: 4.0,
            min_difficulty: 1,
        }
    }
}

impl DaaParams {
    /// Creates parameters for testing.
    pub fn for_testing() -> Self {
        DaaParams {
            target_time_ms: 1000, // 1 second for easier testing
            window_size: 10,
            max_adjustment_factor: 4.0,
            min_difficulty: 1,
        }
    }
}

/// Block timestamp with DAA score.
#[derive(Clone, Copy, Debug)]
pub struct DaaBlock {
    /// Timestamp in milliseconds.
    pub timestamp: u64,
    /// DAA score (cumulative count of blocks).
    pub daa_score: u64,
    /// Difficulty bits.
    pub bits: u32,
}

/// Difficulty manager.
pub struct DifficultyManager {
    /// DAA parameters.
    params: DaaParams,
}

impl DifficultyManager {
    /// Creates a new difficulty manager.
    pub fn new(params: DaaParams) -> Self {
        DifficultyManager { params }
    }

    /// Creates with default parameters.
    pub fn with_defaults() -> Self {
        Self::new(DaaParams::default())
    }

    /// Calculates the next difficulty target.
    ///
    /// Takes a window of recent blocks and computes the adjusted difficulty.
    pub fn calculate_next_difficulty(&self, window: &[DaaBlock]) -> Result<u32, DifficultyError> {
        if window.is_empty() {
            return Err(DifficultyError::EmptyWindow);
        }

        if window.len() == 1 {
            // Not enough blocks, keep same difficulty
            return Ok(window[0].bits);
        }

        // Get timestamps
        let first = &window[0];
        let last = &window[window.len() - 1];

        // Calculate actual time taken
        let actual_time = last.timestamp.saturating_sub(first.timestamp);

        // Calculate expected time
        let blocks_in_window = window.len() as u64 - 1;
        let expected_time = blocks_in_window * self.params.target_time_ms;

        if expected_time == 0 {
            return Err(DifficultyError::InvalidWindow);
        }

        // Calculate adjustment ratio
        let ratio = actual_time as f64 / expected_time as f64;

        // Clamp ratio to prevent extreme adjustments
        let clamped_ratio = ratio.clamp(
            1.0 / self.params.max_adjustment_factor,
            self.params.max_adjustment_factor,
        );

        // Current difficulty
        let current_diff = self.bits_to_difficulty(last.bits);

        // New difficulty = current * ratio
        // (If blocks are too fast, ratio < 1, difficulty increases)
        // (If blocks are too slow, ratio > 1, difficulty decreases)
        let new_diff = (current_diff as f64 / clamped_ratio) as u64;

        // Ensure minimum difficulty
        let final_diff = new_diff.max(self.params.min_difficulty);

        Ok(self.difficulty_to_bits(final_diff))
    }

    /// Converts difficulty bits to target hash.
    pub fn bits_to_target(&self, bits: u32) -> Hash256 {
        // Compact format: bits = exponent || coefficient
        // Target = coefficient * 2^(8*(exponent-3))
        let exponent = (bits >> 24) as u8;
        let coefficient = bits & 0x00FFFFFF;

        if exponent <= 3 {
            let shifted = coefficient >> (8 * (3 - exponent as u32));
            let mut bytes = [0u8; 32];
            bytes[31] = shifted as u8;
            bytes[30] = (shifted >> 8) as u8;
            bytes[29] = (shifted >> 16) as u8;
            return Hash256::from_bytes(bytes);
        }

        let byte_offset = (exponent as usize) - 3;
        let mut bytes = [0u8; 32];

        if byte_offset < 32 {
            bytes[31 - byte_offset] = (coefficient & 0xFF) as u8;
            if byte_offset + 1 < 32 {
                bytes[31 - byte_offset - 1] = ((coefficient >> 8) & 0xFF) as u8;
            }
            if byte_offset + 2 < 32 {
                bytes[31 - byte_offset - 2] = ((coefficient >> 16) & 0xFF) as u8;
            }
        }

        Hash256::from_bytes(bytes)
    }

    /// Converts difficulty bits to numeric difficulty.
    ///
    /// Uses Bitcoin's pool difficulty formula where 0x1d00ffff = difficulty 1.
    /// Formula: difficulty = 0xffff × 256^(0x1d - exponent) / coefficient
    pub fn bits_to_difficulty(&self, bits: u32) -> u64 {
        let exponent = (bits >> 24) as i32;
        let coefficient = (bits & 0x00FFFFFF) as u64;

        if coefficient == 0 {
            return u64::MAX;
        }

        // Reference point: 0x1d00ffff = difficulty 1
        // difficulty = (REF_COEF / coefficient) × 256^(REF_EXP - exponent)
        const REF_EXP: i32 = 0x1d; // 29
        const REF_COEF: u64 = 0xffff; // 65535

        let exp_diff = REF_EXP - exponent;

        if exp_diff == 0 {
            // Same exponent as reference
            REF_COEF / coefficient
        } else if exp_diff > 0 {
            // Smaller target = higher difficulty
            if exp_diff >= 8 {
                // Would overflow, return very high difficulty
                u64::MAX / coefficient
            } else {
                let scale = 256u64.pow(exp_diff as u32);
                REF_COEF.saturating_mul(scale) / coefficient
            }
        } else {
            // Larger target = lower difficulty (exp_diff < 0)
            let abs_exp = (-exp_diff) as u32;
            if abs_exp >= 8 {
                // Very low difficulty
                1
            } else {
                let scale = 256u64.pow(abs_exp);
                REF_COEF / coefficient.saturating_mul(scale)
            }
        }
    }

    /// Converts numeric difficulty to bits.
    ///
    /// Reverse of bits_to_difficulty. Finds exponent and coefficient such that
    /// the resulting bits encodes approximately the given difficulty.
    pub fn difficulty_to_bits(&self, difficulty: u64) -> u32 {
        if difficulty == 0 {
            return 0x207fffff; // Very easy (high target)
        }
        if difficulty == 1 {
            return 0x1d00ffff; // Reference difficulty
        }

        const REF_EXP: i32 = 0x1d; // 29
        const REF_COEF: u64 = 0xffff; // 65535

        // We need: difficulty = REF_COEF × 256^(REF_EXP - exp) / coef
        // Rearranging: coef = REF_COEF × 256^(REF_EXP - exp) / difficulty

        let mut exp = REF_EXP;
        let mut multiplier = 1u64;

        // Calculate initial coefficient at reference exponent
        let mut coef = REF_COEF / difficulty;

        // If coefficient is 0, difficulty is too high - need to increase multiplier
        if coef == 0 {
            while coef == 0 && exp > 3 {
                exp -= 1;
                multiplier = multiplier.saturating_mul(256);
                coef = REF_COEF.saturating_mul(multiplier) / difficulty;
            }
        }

        // Normalize coefficient to valid range [1, 0x7fffff]
        while coef > 0x7fffff && exp < 32 {
            coef /= 256;
            exp += 1;
        }

        // Clamp to valid ranges
        coef = coef.clamp(1, 0x7fffff);
        exp = exp.clamp(3, 32);

        ((exp as u32) << 24) | (coef as u32)
    }

    /// Returns the genesis difficulty bits.
    pub fn genesis_bits() -> u32 {
        // Low difficulty for easy initial mining
        0x207fffff // Very low difficulty
    }

    /// Returns the minimum difficulty bits.
    pub fn min_bits(&self) -> u32 {
        self.difficulty_to_bits(self.params.min_difficulty)
    }

    /// Returns the parameters.
    pub fn params(&self) -> &DaaParams {
        &self.params
    }

    /// Validates that a block's PoW meets the target.
    pub fn validate_pow(&self, block_hash: &Hash256, bits: u32) -> bool {
        let target = self.bits_to_target(bits);
        block_hash <= &target
    }

    /// Estimates hashrate from difficulty and block time.
    pub fn estimate_hashrate(&self, bits: u32) -> f64 {
        let difficulty = self.bits_to_difficulty(bits) as f64;
        let block_time_seconds = self.params.target_time_ms as f64 / 1000.0;

        // Hashrate ≈ difficulty * 2^32 / block_time
        difficulty * 4_294_967_296.0 / block_time_seconds
    }
}

impl Default for DifficultyManager {
    fn default() -> Self {
        Self::with_defaults()
    }
}

impl std::fmt::Debug for DifficultyManager {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("DifficultyManager")
            .field("params", &self.params)
            .finish()
    }
}

/// Errors related to difficulty calculations.
#[derive(Debug, Error)]
pub enum DifficultyError {
    #[error("Empty block window")]
    EmptyWindow,

    #[error("Invalid block window")]
    InvalidWindow,

    #[error("Difficulty overflow")]
    Overflow,
}

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

    #[test]
    fn test_bits_roundtrip() {
        let manager = DifficultyManager::with_defaults();

        // Test reference difficulty 1
        let original_bits = 0x1d00ffff;
        let difficulty = manager.bits_to_difficulty(original_bits);
        assert_eq!(difficulty, 1);
        let back_to_bits = manager.difficulty_to_bits(difficulty);
        assert_eq!(back_to_bits, original_bits);

        // Test higher difficulty (0x1c00ffff ≈ 256)
        let harder_bits = 0x1c00ffff;
        let harder_diff = manager.bits_to_difficulty(harder_bits);
        assert_eq!(harder_diff, 256);
        let back_to_harder = manager.difficulty_to_bits(harder_diff);
        // May not be exact, but converted back should give same difficulty
        let roundtrip_diff = manager.bits_to_difficulty(back_to_harder);
        assert!(
            (roundtrip_diff as i64 - harder_diff as i64).abs() < 10,
            "Expected ~256, got {} after roundtrip",
            roundtrip_diff
        );
    }

    #[test]
    fn test_genesis_bits() {
        let bits = DifficultyManager::genesis_bits();
        assert!(bits > 0);
    }

    #[test]
    fn test_calculate_difficulty_stable() {
        let params = DaaParams::for_testing();
        let manager = DifficultyManager::new(params.clone());

        // Use 0x1c00ffff (difficulty ~256) which has room to adjust both ways
        let starting_bits = 0x1c00ffff;

        // Create window where blocks came at exactly target rate
        let window: Vec<DaaBlock> = (0..10)
            .map(|i| DaaBlock {
                timestamp: i * params.target_time_ms,
                daa_score: i,
                bits: starting_bits,
            })
            .collect();

        let new_bits = manager.calculate_next_difficulty(&window).unwrap();

        // Difficulty should stay similar (within max adjustment)
        let old_diff = manager.bits_to_difficulty(starting_bits);
        let new_diff = manager.bits_to_difficulty(new_bits);
        let ratio = new_diff as f64 / old_diff as f64;

        assert!(
            ratio > 0.25 && ratio < 4.0,
            "Expected ratio in (0.25, 4.0), got {} (old={}, new={})",
            ratio,
            old_diff,
            new_diff
        );
    }

    #[test]
    fn test_calculate_difficulty_fast_blocks() {
        let params = DaaParams::for_testing();
        let manager = DifficultyManager::new(params.clone());

        // Use 0x1c00ffff (difficulty ~256) which has room to adjust both ways
        let starting_bits = 0x1c00ffff;

        // Create window where blocks came too fast (half target time)
        let window: Vec<DaaBlock> = (0..10)
            .map(|i| DaaBlock {
                timestamp: i * params.target_time_ms / 2,
                daa_score: i,
                bits: starting_bits,
            })
            .collect();

        let new_bits = manager.calculate_next_difficulty(&window).unwrap();

        // Difficulty should increase (bits should decrease)
        let old_diff = manager.bits_to_difficulty(starting_bits);
        let new_diff = manager.bits_to_difficulty(new_bits);

        assert!(
            new_diff > old_diff,
            "Expected difficulty to increase: old={}, new={}",
            old_diff,
            new_diff
        );
    }

    #[test]
    fn test_calculate_difficulty_slow_blocks() {
        let params = DaaParams::for_testing();
        let manager = DifficultyManager::new(params.clone());

        // Use 0x1c00ffff (difficulty ~256) which has room to decrease
        let starting_bits = 0x1c00ffff;

        // Create window where blocks came too slow (double target time)
        let window: Vec<DaaBlock> = (0..10)
            .map(|i| DaaBlock {
                timestamp: i * params.target_time_ms * 2,
                daa_score: i,
                bits: starting_bits,
            })
            .collect();

        let new_bits = manager.calculate_next_difficulty(&window).unwrap();

        // Difficulty should decrease (bits should increase)
        let old_diff = manager.bits_to_difficulty(starting_bits);
        let new_diff = manager.bits_to_difficulty(new_bits);

        assert!(
            new_diff < old_diff,
            "Expected difficulty to decrease: old={}, new={}",
            old_diff,
            new_diff
        );
    }

    #[test]
    fn test_empty_window_error() {
        let manager = DifficultyManager::with_defaults();
        let result = manager.calculate_next_difficulty(&[]);
        assert!(matches!(result, Err(DifficultyError::EmptyWindow)));
    }
}
