pub use pythnet_sdk::messages::{
    FeedId,
    PriceFeedMessage,
};
use {
    crate::{
        check,
        error::GetPriceError,
    },
    anchor_lang::prelude::{
        borsh::BorshSchema,
        *,
    },
    solana_program::pubkey::Pubkey,
};


/// Pyth price updates are bridged to all blockchains via Wormhole.
/// Using the price updates on another chain requires verifying the signatures of the Wormhole guardians.
/// The usual process is to check the signatures for two thirds of the total number of guardians, but this can be cumbersome on Solana because of the transaction size limits,
/// so we also allow for partial verification.
///
/// This enum represents how much a price update has been verified:
/// - If `Full`, we have verified the signatures for two thirds of the current guardians.
/// - If `Partial`, only `num_signatures` guardian signatures have been checked.
///
/// # Warning
/// Using partially verified price updates is dangerous, as it lowers the threshold of guardians that need to collude to produce a malicious price update.
#[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, BorshSchema, Debug)]
pub enum VerificationLevel {
    Partial { num_signatures: u8 },
    Full,
}

impl VerificationLevel {
    /// Compare two `VerificationLevel`.
    /// `Full` is always greater than `Partial`, and `Partial` with more signatures is greater than `Partial` with fewer signatures.
    pub fn gte(&self, other: VerificationLevel) -> bool {
        match self {
            VerificationLevel::Full => true,
            VerificationLevel::Partial { num_signatures } => match other {
                VerificationLevel::Full => false,
                VerificationLevel::Partial {
                    num_signatures: other_num_signatures,
                } => *num_signatures >= other_num_signatures,
            },
        }
    }
}

/// A price update account. This account is used by the Pyth Receiver program to store a verified price update from a Pyth price feed.
/// It contains:
/// - `write_authority`: The write authority for this account. This authority can close this account to reclaim rent or update the account to contain a different price update.
/// - `verification_level`: The [`VerificationLevel`] of this price update. This represents how many Wormhole guardian signatures have been verified for this price update.
/// - `price_message`: The actual price update.
/// - `posted_slot`: The slot at which this price update was posted.
#[account]
#[derive(BorshSchema)]
pub struct PriceUpdateV2 {
    pub write_authority:    Pubkey,
    pub verification_level: VerificationLevel,
    pub price_message:      PriceFeedMessage,
    pub posted_slot:        u64,
}

impl PriceUpdateV2 {
    pub const LEN: usize = 8 + 32 + 2 + 32 + 8 + 8 + 4 + 8 + 8 + 8 + 8 + 8;
}

/// A Pyth price.
/// The actual price is `(price ± conf)* 10^exponent`. `publish_time` may be used to check the recency of the price.
#[derive(PartialEq, Debug, Clone, Copy)]
pub struct Price {
    pub price:        i64,
    pub conf:         u64,
    pub exponent:     i32,
    pub publish_time: i64,
}

impl PriceUpdateV2 {
    /// Get a `Price` from a `PriceUpdateV2` account for a given `FeedId`.
    ///
    /// # Warning
    /// This function does not check :
    /// - How recent the price is
    /// - Whether the price update has been verified
    ///
    /// It is therefore unsafe to use this function without any extra checks, as it allows for the possibility of using unverified or outdated price updates.
    pub fn get_price_unchecked(
        &self,
        feed_id: &FeedId,
    ) -> std::result::Result<Price, GetPriceError> {
        check!(
            self.price_message.feed_id == *feed_id,
            GetPriceError::MismatchedFeedId
        );
        Ok(Price {
            price:        self.price_message.price,
            conf:         self.price_message.conf,
            exponent:     self.price_message.exponent,
            publish_time: self.price_message.publish_time,
        })
    }

    /// Get a `Price` from a `PriceUpdateV2` account for a given `FeedId` no older than `maximum_age` with customizable verification level.
    ///
    /// # Warning
    /// Lowering the verification level from `Full` to `Partial` increases the risk of using a malicious price update.
    /// Please read the documentation for [`VerificationLevel`] for more information.
    ///
    /// # Example
    /// ```
    /// use pyth_solana_receiver_sdk::price_update::{get_feed_id_from_hex, VerificationLevel, PriceUpdateV2};
    /// use anchor_lang::prelude::*;
    ///
    /// const MAXIMUM_AGE : u64 = 30;
    /// const FEED_ID: &str = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; // SOL/USD
    ///
    /// #[derive(Accounts)]
    /// #[instruction(amount_in_usd : u64)]
    /// pub struct ReadPriceAccount<'info> {
    ///     pub price_update: Account<'info, PriceUpdateV2>,
    /// }
    ///
    /// pub fn read_price_account(ctx : Context<ReadPriceAccount>) -> Result<()> {
    ///     let price_update = &mut ctx.accounts.price_update;
    ///     let price = price_update.get_price_no_older_than_with_custom_verification_level(&Clock::get()?, MAXIMUM_AGE, &get_feed_id_from_hex(FEED_ID)?, VerificationLevel::Partial{num_signatures: 5})?;
    ///     Ok(())
    /// }
    ///```
    pub fn get_price_no_older_than_with_custom_verification_level(
        &self,
        clock: &Clock,
        maximum_age: u64,
        feed_id: &FeedId,
        verification_level: VerificationLevel,
    ) -> std::result::Result<Price, GetPriceError> {
        check!(
            self.verification_level.gte(verification_level),
            GetPriceError::InsufficientVerificationLevel
        );
        let price = self.get_price_unchecked(feed_id)?;
        check!(
            price
                .publish_time
                .saturating_add(maximum_age.try_into().unwrap())
                >= clock.unix_timestamp,
            GetPriceError::PriceTooOld
        );
        Ok(price)
    }

    /// Get a `Price` from a `PriceUpdateV2` account for a given `FeedId` no older than `maximum_age` with `Full` verification.
    ///
    /// # Example
    /// ```
    /// use pyth_solana_receiver_sdk::price_update::{get_feed_id_from_hex, PriceUpdateV2};
    /// use anchor_lang::prelude::*;
    ///
    /// const MAXIMUM_AGE : u64 = 30;
    /// const FEED_ID: &str = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; // SOL/USD
    ///
    /// #[derive(Accounts)]
    /// #[instruction(amount_in_usd : u64)]
    /// pub struct ReadPriceAccount<'info> {
    ///     pub price_update: Account<'info, PriceUpdateV2>,
    /// }
    ///
    /// pub fn read_price_account(ctx : Context<ReadPriceAccount>) -> Result<()> {
    ///     let price_update = &mut ctx.accounts.price_update;
    ///     let price = price_update.get_price_no_older_than(&Clock::get()?, MAXIMUM_AGE, &get_feed_id_from_hex(FEED_ID)?)?;
    ///     Ok(())
    /// }
    ///```
    pub fn get_price_no_older_than(
        &self,
        clock: &Clock,
        maximum_age: u64,
        feed_id: &FeedId,
    ) -> std::result::Result<Price, GetPriceError> {
        self.get_price_no_older_than_with_custom_verification_level(
            clock,
            maximum_age,
            feed_id,
            VerificationLevel::Full,
        )
    }
}

/// Get a `FeedId` from a hex string.
///
/// Price feed ids are a 32 byte unique identifier for each price feed in the Pyth network.
/// They are sometimes represented as a 64 character hex string (with or without a 0x prefix).
///
/// # Example
///
/// ```
/// use pyth_solana_receiver_sdk::price_update::get_feed_id_from_hex;
/// let feed_id = get_feed_id_from_hex("0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d").unwrap();
/// ```
pub fn get_feed_id_from_hex(input: &str) -> std::result::Result<FeedId, GetPriceError> {
    let mut feed_id: FeedId = [0; 32];
    match input.len() {
        66 => feed_id.copy_from_slice(
            &hex::decode(&input[2..]).map_err(|_| GetPriceError::FeedIdNonHexCharacter)?,
        ),
        64 => feed_id.copy_from_slice(
            &hex::decode(input).map_err(|_| GetPriceError::FeedIdNonHexCharacter)?,
        ),
        _ => return Err(GetPriceError::FeedIdMustBe32Bytes),
    }
    Ok(feed_id)
}

#[cfg(test)]
pub mod tests {
    use {
        crate::{
            error::GetPriceError,
            price_update::{
                Price,
                PriceUpdateV2,
                VerificationLevel,
            },
        },
        anchor_lang::Discriminator,
        pythnet_sdk::messages::PriceFeedMessage,
        solana_program::{
            borsh0_10,
            clock::Clock,
            pubkey::Pubkey,
        },
    };

    #[test]
    fn check_size() {
        assert!(
            PriceUpdateV2::discriminator().len() + borsh0_10::get_packed_len::<PriceUpdateV2>()
                == PriceUpdateV2::LEN
        );
    }

    #[test]
    fn gte() {
        assert!(VerificationLevel::Full.gte(VerificationLevel::Full));
        assert!(VerificationLevel::Full.gte(VerificationLevel::Partial {
            num_signatures: 255,
        }));
        assert!(VerificationLevel::Partial { num_signatures: 8 }
            .gte(VerificationLevel::Partial { num_signatures: 8 }));
        assert!(!VerificationLevel::Partial { num_signatures: 8 }.gte(VerificationLevel::Full));
        assert!(!VerificationLevel::Partial { num_signatures: 8 }
            .gte(VerificationLevel::Partial { num_signatures: 9 }));
    }

    #[test]
    fn get_feed_id_from_hex() {
        let feed_id = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d";
        let expected_feed_id = [
            239, 13, 139, 111, 218, 44, 235, 164, 29, 161, 93, 64, 149, 209, 218, 57, 42, 13, 47,
            142, 208, 198, 199, 188, 15, 76, 250, 200, 194, 128, 181, 109,
        ];
        assert_eq!(super::get_feed_id_from_hex(feed_id), Ok(expected_feed_id));
        assert_eq!(
            super::get_feed_id_from_hex(&feed_id[2..]),
            Ok(expected_feed_id)
        );

        assert_eq!(
            super::get_feed_id_from_hex(&feed_id[..64]),
            Err(GetPriceError::FeedIdNonHexCharacter)
        );

        assert_eq!(
            super::get_feed_id_from_hex(
                "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b5"
            ),
            Err(GetPriceError::FeedIdMustBe32Bytes)
        );
    }

    #[test]
    fn get_price() {
        let expected_price = Price {
            price:        1,
            conf:         2,
            exponent:     3,
            publish_time: 900,
        };

        let feed_id = [0; 32];
        let mismatched_feed_id = [1; 32];
        let mock_clock = Clock {
            unix_timestamp: 1000,
            ..Default::default()
        };

        let price_update_unverified = PriceUpdateV2 {
            write_authority:    Pubkey::new_unique(),
            verification_level: VerificationLevel::Partial { num_signatures: 0 },
            price_message:      PriceFeedMessage {
                feed_id,
                ema_conf: 0,
                ema_price: 0,
                price: 1,
                conf: 2,
                exponent: 3,
                prev_publish_time: 899,
                publish_time: 900,
            },
            posted_slot:        0,
        };

        let price_update_partially_verified = PriceUpdateV2 {
            write_authority:    Pubkey::new_unique(),
            verification_level: VerificationLevel::Partial { num_signatures: 5 },
            price_message:      PriceFeedMessage {
                feed_id,
                ema_conf: 0,
                ema_price: 0,
                price: 1,
                conf: 2,
                exponent: 3,
                prev_publish_time: 899,
                publish_time: 900,
            },
            posted_slot:        0,
        };

        let price_update_fully_verified = PriceUpdateV2 {
            write_authority:    Pubkey::new_unique(),
            verification_level: VerificationLevel::Full,
            price_message:      PriceFeedMessage {
                feed_id,
                ema_conf: 0,
                ema_price: 0,
                price: 1,
                conf: 2,
                exponent: 3,
                prev_publish_time: 899,
                publish_time: 900,
            },
            posted_slot:        0,
        };


        assert_eq!(
            price_update_unverified.get_price_unchecked(&feed_id),
            Ok(expected_price)
        );
        assert_eq!(
            price_update_partially_verified.get_price_unchecked(&feed_id),
            Ok(expected_price)
        );
        assert_eq!(
            price_update_fully_verified.get_price_unchecked(&feed_id),
            Ok(expected_price)
        );

        assert_eq!(
            price_update_unverified.get_price_no_older_than_with_custom_verification_level(
                &mock_clock,
                100,
                &feed_id,
                VerificationLevel::Partial { num_signatures: 5 }
            ),
            Err(GetPriceError::InsufficientVerificationLevel)
        );
        assert_eq!(
            price_update_partially_verified.get_price_no_older_than_with_custom_verification_level(
                &mock_clock,
                100,
                &feed_id,
                VerificationLevel::Partial { num_signatures: 5 }
            ),
            Ok(expected_price)
        );
        assert_eq!(
            price_update_fully_verified.get_price_no_older_than_with_custom_verification_level(
                &mock_clock,
                100,
                &feed_id,
                VerificationLevel::Partial { num_signatures: 5 }
            ),
            Ok(expected_price)
        );

        assert_eq!(
            price_update_unverified.get_price_no_older_than(&mock_clock, 100, &feed_id,),
            Err(GetPriceError::InsufficientVerificationLevel)
        );
        assert_eq!(
            price_update_partially_verified.get_price_no_older_than(&mock_clock, 100, &feed_id,),
            Err(GetPriceError::InsufficientVerificationLevel)
        );
        assert_eq!(
            price_update_fully_verified.get_price_no_older_than(&mock_clock, 100, &feed_id,),
            Ok(expected_price)
        );

        // Reduce maximum_age
        assert_eq!(
            price_update_unverified.get_price_no_older_than_with_custom_verification_level(
                &mock_clock,
                10,
                &feed_id,
                VerificationLevel::Partial { num_signatures: 5 }
            ),
            Err(GetPriceError::InsufficientVerificationLevel)
        );
        assert_eq!(
            price_update_partially_verified.get_price_no_older_than_with_custom_verification_level(
                &mock_clock,
                10,
                &feed_id,
                VerificationLevel::Partial { num_signatures: 5 }
            ),
            Err(GetPriceError::PriceTooOld)
        );
        assert_eq!(
            price_update_fully_verified.get_price_no_older_than_with_custom_verification_level(
                &mock_clock,
                10,
                &feed_id,
                VerificationLevel::Partial { num_signatures: 5 }
            ),
            Err(GetPriceError::PriceTooOld)
        );

        assert_eq!(
            price_update_unverified.get_price_no_older_than(&mock_clock, 10, &feed_id,),
            Err(GetPriceError::InsufficientVerificationLevel)
        );
        assert_eq!(
            price_update_partially_verified.get_price_no_older_than(&mock_clock, 10, &feed_id,),
            Err(GetPriceError::InsufficientVerificationLevel)
        );
        assert_eq!(
            price_update_fully_verified.get_price_no_older_than(&mock_clock, 10, &feed_id,),
            Err(GetPriceError::PriceTooOld)
        );

        // Mismatched feed id
        assert_eq!(
            price_update_fully_verified.get_price_unchecked(&mismatched_feed_id),
            Err(GetPriceError::MismatchedFeedId)
        );
        assert_eq!(
            price_update_fully_verified.get_price_no_older_than_with_custom_verification_level(
                &mock_clock,
                100,
                &mismatched_feed_id,
                VerificationLevel::Partial { num_signatures: 5 }
            ),
            Err(GetPriceError::MismatchedFeedId)
        );
        assert_eq!(
            price_update_fully_verified.get_price_no_older_than(
                &mock_clock,
                100,
                &mismatched_feed_id,
            ),
            Err(GetPriceError::MismatchedFeedId)
        );
    }
}
