use serde::{Deserialize, Serialize};
use steel::*;

use crate::state::{stake_pda, Pool};

use super::OilAccount;

#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, Serialize, Deserialize)]
pub struct Stake {
    /// The authority of this driller account.
    pub authority: Pubkey,

    /// The balance of this stake account.
    pub balance: u64,

    /// Lock duration in days (0 = no lock, 1-730 days)
    pub lock_duration_days: u64,

    /// Unix timestamp when lock expires (0 = no lock)
    pub lock_ends_at: u64,

    /// Buffer c (placeholder)
    pub buffer_c: u64,

    /// Buffer d (placeholder)
    pub buffer_d: u64,

    /// Buffer e (placeholder)
    pub buffer_e: u64,

    /// The timestamp of last claim.
    pub last_claim_at: i64,

    /// The timestamp the last time this staker deposited.
    pub last_deposit_at: i64,

    /// The timestamp the last time this staker withdrew.
    pub last_withdraw_at: i64,

    /// The rewards factor last time rewards were updated on this stake account.
    pub rewards_factor: Numeric,

    /// The amount of SOL this staker can claim.
    pub rewards: u64,

    /// The total amount of SOL this staker has earned over its lifetime.
    pub lifetime_rewards: u64,

    /// Buffer f (placeholder)
    pub buffer_f: u64,
}

impl Stake {
    pub fn pda(&self) -> (Pubkey, u8) {
        stake_pda(self.authority)
    }

    /// Calculate lock multiplier based on duration (Magic Eden formula)
    /// Returns multiplier (1.0 = no lock, up to 20.0 for 730 days)
    pub fn calculate_multiplier(lock_duration_days: u64) -> f64 {
        if lock_duration_days == 0 {
            return 1.0;
        }

        // Magic Eden lookup table (exact values from GODL)
        let lookup: [(u64, f64); 6] = [
            (7, 1.18),
            (30, 1.78),
            (90, 3.35),
            (180, 5.69),
            (365, 10.5),
            (730, 20.0),
        ];

        // Cap at 730 days
        let days = lock_duration_days.min(730);

        // Exact match
        for &(d, m) in &lookup {
            if days == d {
                return m;
            }
        }

        // Linear interpolation between lookup points
        for i in 0..lookup.len() - 1 {
            let (d1, m1) = lookup[i];
            let (d2, m2) = lookup[i + 1];

            if days >= d1 && days <= d2 {
                let ratio = (days - d1) as f64 / (d2 - d1) as f64;
                return m1 + (m2 - m1) * ratio;
            }
        }

        // Extrapolate beyond 730 days (shouldn't happen, but cap at 20.0)
        if days > 730 {
            return 20.0;
        }

        // Below 7 days: linear from 1.0 to 1.18
        if days < 7 {
            return 1.0 + (days as f64 / 7.0) * 0.18;
        }

        1.0 // Fallback
    }

    /// Calculate score (balance * multiplier)
    pub fn score(&self) -> u64 {
        let multiplier = Self::calculate_multiplier(self.lock_duration_days);
        // Use fixed-point arithmetic: multiply by 1_000_000 for precision, then divide
        // Result fits in u64: max balance (2.1M OIL) * max multiplier (20.0) = 42M << u64::MAX
        ((self.balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64
    }

    /// Check if stake is currently locked
    pub fn is_locked(&self, clock: &Clock) -> bool {
        self.lock_ends_at > 0 && (clock.unix_timestamp as u64) < self.lock_ends_at
    }

    /// Get remaining lock time in seconds (0 if unlocked)
    pub fn remaining_lock_seconds(&self, clock: &Clock) -> u64 {
        if self.lock_ends_at == 0 || (clock.unix_timestamp as u64) >= self.lock_ends_at {
            return 0;
        }
        self.lock_ends_at - (clock.unix_timestamp as u64)
    }

    pub fn claim(&mut self, amount: u64, clock: &Clock, pool: &Pool) -> u64 {
        self.update_rewards(pool);
        let amount = self.rewards.min(amount);
        self.rewards -= amount;
        self.last_claim_at = clock.unix_timestamp;
        amount
    }

    pub fn deposit(
        &mut self,
        amount: u64,
        clock: &Clock,
        pool: &mut Pool,
        sender: &TokenAccount,
    ) -> u64 {
        self.update_rewards(pool);
        
        // Calculate old score before deposit
        let old_score = self.score();
        
        let amount = sender.amount().min(amount);
        self.balance += amount;
        self.last_deposit_at = clock.unix_timestamp;
        
        // Calculate new score after deposit
        let new_score = self.score();
        
        // Update pool totals
        pool.total_staked += amount;
        pool.total_staked_score = pool.total_staked_score.saturating_add(new_score).saturating_sub(old_score);
        
        amount
    }

    pub fn withdraw(&mut self, amount: u64, clock: &Clock, pool: &mut Pool) -> u64 {
        self.update_rewards(pool);
        
        // Calculate old score before withdraw
        let old_score = self.score();
        
        let amount = self.balance.min(amount);
        self.balance -= amount;
        self.last_withdraw_at = clock.unix_timestamp;
        
        // Calculate new score after withdraw
        let new_score = self.score();
        
        // Update pool totals
        pool.total_staked -= amount;
        pool.total_staked_score = pool.total_staked_score.saturating_add(new_score).saturating_sub(old_score);
        
        amount
    }

    pub fn update_rewards(&mut self, pool: &Pool) {
        // Accumulate SOL rewards, weighted by stake score (balance * multiplier).
        if pool.stake_rewards_factor > self.rewards_factor {
            let accumulated_rewards = pool.stake_rewards_factor - self.rewards_factor;
            if accumulated_rewards < Numeric::ZERO {
                panic!("Accumulated rewards is negative");
            }
            // Use score instead of balance for lock-based weighted staking
            let score = self.score();
            let personal_rewards = accumulated_rewards * Numeric::from_u64(score);
            self.rewards += personal_rewards.to_u64();
            self.lifetime_rewards += personal_rewards.to_u64();
        }

        // Update this stake account's last seen rewards factor.
        self.rewards_factor = pool.stake_rewards_factor;
    }
}

account!(OilAccount, Stake);

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

    #[test]
    fn test_multiplier_exact_lookup_values() {
        // Test exact lookup table values
        assert_eq!(Stake::calculate_multiplier(7), 1.18);
        assert_eq!(Stake::calculate_multiplier(30), 1.78);
        assert_eq!(Stake::calculate_multiplier(90), 3.35);
        assert_eq!(Stake::calculate_multiplier(180), 5.69);
        assert_eq!(Stake::calculate_multiplier(365), 10.5);
        assert_eq!(Stake::calculate_multiplier(730), 20.0);
    }

    #[test]
    fn test_multiplier_no_lock() {
        // No lock should return 1.0
        assert_eq!(Stake::calculate_multiplier(0), 1.0);
    }

    #[test]
    fn test_multiplier_below_7_days() {
        // Linear interpolation from 1.0 to 1.18 for days < 7
        assert_eq!(Stake::calculate_multiplier(1), 1.0 + (1.0 / 7.0) * 0.18);
        assert_eq!(Stake::calculate_multiplier(3), 1.0 + (3.0 / 7.0) * 0.18);
        assert_eq!(Stake::calculate_multiplier(6), 1.0 + (6.0 / 7.0) * 0.18);
        
        // Verify it's less than 1.18
        assert!(Stake::calculate_multiplier(6) < 1.18);
    }

    #[test]
    fn test_multiplier_interpolation_between_lookup_points() {
        // Test interpolation between 7 and 30 days
        let multiplier_15 = Stake::calculate_multiplier(15);
        assert!(multiplier_15 > 1.18);
        assert!(multiplier_15 < 1.78);
        
        // Test interpolation between 30 and 90 days
        let multiplier_60 = Stake::calculate_multiplier(60);
        assert!(multiplier_60 > 1.78);
        assert!(multiplier_60 < 3.35);
        
        // Test interpolation between 90 and 180 days
        let multiplier_135 = Stake::calculate_multiplier(135);
        assert!(multiplier_135 > 3.35);
        assert!(multiplier_135 < 5.69);
        
        // Test interpolation between 180 and 365 days
        let multiplier_272 = Stake::calculate_multiplier(272);
        assert!(multiplier_272 > 5.69);
        assert!(multiplier_272 < 10.5);
        
        // Test interpolation between 365 and 730 days
        let multiplier_547 = Stake::calculate_multiplier(547);
        assert!(multiplier_547 > 10.5);
        assert!(multiplier_547 < 20.0);
    }

    #[test]
    fn test_multiplier_above_730_days() {
        // Should cap at 20.0 for days > 730
        assert_eq!(Stake::calculate_multiplier(730), 20.0);
        assert_eq!(Stake::calculate_multiplier(1000), 20.0);
        assert_eq!(Stake::calculate_multiplier(u64::MAX), 20.0);
    }

    #[test]
    fn test_score_calculation() {
        // Create a mock stake for testing
        // Note: We can't easily create a full Stake struct without all dependencies,
        // so we'll test the score calculation logic directly
        
        // Test with no lock (multiplier = 1.0)
        let balance = 1000u64;
        let multiplier = Stake::calculate_multiplier(0);
        let expected_score = ((balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
        assert_eq!(expected_score, 1000);
        
        // Test with 7-day lock (multiplier = 1.18)
        let multiplier = Stake::calculate_multiplier(7);
        let expected_score = ((balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
        assert_eq!(expected_score, 1180);
        
        // Test with 730-day lock (multiplier = 20.0)
        let multiplier = Stake::calculate_multiplier(730);
        let expected_score = ((balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
        assert_eq!(expected_score, 20000);
    }

    #[test]
    fn test_score_with_large_balance() {
        // Test with maximum realistic balance (2.1M OIL)
        let max_balance = 2_100_000u64;
        let multiplier = Stake::calculate_multiplier(730); // 20.0x
        let score = ((max_balance as u128 * (multiplier * 1_000_000.0) as u128) / 1_000_000) as u64;
        
        // Max score = 2.1M * 20 = 42M, which fits in u64
        assert_eq!(score, 42_000_000);
        assert!(score < u64::MAX);
    }

    #[test]
    fn test_multiplier_linear_interpolation_accuracy() {
        // Test that interpolation is linear between points
        // Between 7 and 30 days: 15 days should be halfway
        let m7 = Stake::calculate_multiplier(7);
        let m30 = Stake::calculate_multiplier(30);
        let m15 = Stake::calculate_multiplier(15);
        
        // 15 is (15-7)/(30-7) = 8/23 of the way from 7 to 30
        let ratio = (15.0 - 7.0) / (30.0 - 7.0);
        let expected = m7 + ratio * (m30 - m7);
        
        // Allow small floating point error
        assert!((m15 - expected).abs() < 0.0001);
    }

    #[test]
    fn test_multiplier_monotonic_increase() {
        // Verify multipliers increase monotonically with lock duration
        let mut prev_multiplier = 0.0;
        for days in [0, 1, 7, 15, 30, 60, 90, 135, 180, 272, 365, 547, 730, 1000] {
            let multiplier = Stake::calculate_multiplier(days);
            assert!(multiplier >= prev_multiplier, 
                "Multiplier should not decrease: {} days = {}, previous = {}", 
                days, multiplier, prev_multiplier);
            prev_multiplier = multiplier;
        }
    }

    #[test]
    fn test_multiplier_boundaries() {
        // Test boundary conditions
        assert_eq!(Stake::calculate_multiplier(0), 1.0);
        assert!(Stake::calculate_multiplier(1) > 1.0);
        assert!(Stake::calculate_multiplier(1) < 1.18);
        assert_eq!(Stake::calculate_multiplier(7), 1.18);
        assert_eq!(Stake::calculate_multiplier(730), 20.0);
        assert_eq!(Stake::calculate_multiplier(731), 20.0);
    }
}