#[cfg(feature = "proto")]
use prost::Message;

use crate::{instrument_type::InstrumentType, pair::Pair, web3::Chain};

#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
    feature = "borsh",
    derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct PriceEntry {
    pub source: String,
    pub chain: Option<Chain>,
    pub pair: Pair,
    pub timestamp_ms: i64,
    pub price: u128,
    pub volume: u128,
    pub expiration_timestamp: Option<i64>,
    pub instrument_type: InstrumentType,
    pub received_timestamp_ms: i64,
}


#[cfg(feature = "proto")]
impl PriceEntry {
    fn to_proto(&self) -> crate::schema::PriceEntry {
        crate::schema::PriceEntry {
            source: self.source.clone(),
            chain_option: match &self.chain {
                Some(chain) => Some(crate::schema::price_entry::ChainOption::Chain(
                    match chain {
                        Chain::Starknet => crate::schema::Chain::Starknet as i32,
                        Chain::Solana => crate::schema::Chain::Solana as i32,
                        Chain::Sui => crate::schema::Chain::Sui as i32,
                        Chain::Aptos => crate::schema::Chain::Aptos as i32,
                        Chain::Ethereum => crate::schema::Chain::Ethereum as i32,
                        Chain::Base => crate::schema::Chain::Base as i32,
                        Chain::Arbitrum => crate::schema::Chain::Arbitrum as i32,
                        Chain::Optimism => crate::schema::Chain::Optimism as i32,
                        Chain::ZkSync => crate::schema::Chain::Zksync as i32,
                        Chain::Polygon => crate::schema::Chain::Polygon as i32,
                        Chain::Bnb => crate::schema::Chain::Bnb as i32,
                        Chain::Avalanche => crate::schema::Chain::Avalanche as i32,
                        Chain::Gnosis => crate::schema::Chain::Gnosis as i32,
                        Chain::Worldchain => crate::schema::Chain::Worldchain as i32,
                    },
                )),
                None => Some(crate::schema::price_entry::ChainOption::NoChain(true)),
            },
            pair: Some(crate::schema::Pair {
                base: self.pair.base.clone(),
                quote: self.pair.quote.clone(),
            }),
            timestamp_ms: self.timestamp_ms,
            price: Some(crate::schema::UInt128 {
                low: self.price as u64,
                high: (self.price >> 64) as u64,
            }),
            volume: Some(crate::schema::UInt128 {
                low: self.volume as u64,
                high: (self.volume >> 64) as u64,
            }),
            expiration_option: Some(match self.expiration_timestamp {
                Some(ts) => crate::schema::price_entry::ExpirationOption::ExpirationTimestamp(ts),
                None => crate::schema::price_entry::ExpirationOption::NoExpiration(true),
            }),
            instrument_type: match self.instrument_type {
                InstrumentType::Spot => crate::schema::InstrumentType::Spot as i32,
                InstrumentType::Perp => crate::schema::InstrumentType::Perp as i32,
            },
            received_timestamp_ms: self.received_timestamp_ms,
        }
    }

    fn from_proto(proto: crate::schema::PriceEntry) -> Result<Self, prost::DecodeError> {
        let chain = match proto.chain_option {
            Some(crate::schema::price_entry::ChainOption::NoChain(_)) => None,
            Some(crate::schema::price_entry::ChainOption::Chain(chain)) => Some(match chain {
                x if x == crate::schema::Chain::Starknet as i32 => Chain::Starknet,
                x if x == crate::schema::Chain::Solana as i32 => Chain::Solana,
                x if x == crate::schema::Chain::Sui as i32 => Chain::Sui,
                x if x == crate::schema::Chain::Aptos as i32 => Chain::Aptos,
                x if x == crate::schema::Chain::Ethereum as i32 => Chain::Ethereum,
                x if x == crate::schema::Chain::Base as i32 => Chain::Base,
                x if x == crate::schema::Chain::Arbitrum as i32 => Chain::Arbitrum,
                x if x == crate::schema::Chain::Optimism as i32 => Chain::Optimism,
                x if x == crate::schema::Chain::Zksync as i32 => Chain::ZkSync,
                x if x == crate::schema::Chain::Polygon as i32 => Chain::Polygon,
                x if x == crate::schema::Chain::Bnb as i32 => Chain::Bnb,
                x if x == crate::schema::Chain::Avalanche as i32 => Chain::Avalanche,
                x if x == crate::schema::Chain::Gnosis as i32 => Chain::Gnosis,
                x if x == crate::schema::Chain::Worldchain as i32 => Chain::Worldchain,
                _ => {
                    return Err(prost::DecodeError::new(format!(
                        "Unknown chain value: {chain}",
                    )))
                }
            }),
            None => {
                return Err(prost::DecodeError::new(
                    "Missing chain_option field in PriceEntry".to_string(),
                ))
            }
        };

        let pair = proto
            .pair
            .ok_or_else(|| prost::DecodeError::new("Missing pair field in PriceEntry"))?;
        let pair = Pair {
            base: pair.base,
            quote: pair.quote,
        };

        let price = proto
            .price
            .ok_or_else(|| prost::DecodeError::new("Missing price field in PriceEntry"))?;
        let price = (price.high as u128) << 64 | price.low as u128;

        let volume = proto
            .volume
            .ok_or_else(|| prost::DecodeError::new("Missing volume field in PriceEntry"))?;
        let volume = (volume.high as u128) << 64 | volume.low as u128;

        let expiration_timestamp = match proto.expiration_option {
            Some(crate::schema::price_entry::ExpirationOption::NoExpiration(_)) => None,
            Some(crate::schema::price_entry::ExpirationOption::ExpirationTimestamp(ts)) => Some(ts),
            None => {
                return Err(prost::DecodeError::new(
                    "Missing expiration_option field in PriceEntry".to_string(),
                ))
            }
        };

        let instrument_type = match proto.instrument_type {
            x if x == crate::schema::InstrumentType::Spot as i32 => InstrumentType::Spot,
            x if x == crate::schema::InstrumentType::Perp as i32 => InstrumentType::Perp,
            _ => InstrumentType::Spot, // Default for backwards compatibility
        };

        Ok(PriceEntry {
            source: proto.source,
            chain,
            pair,
            timestamp_ms: proto.timestamp_ms,
            price,
            volume,
            expiration_timestamp,
            instrument_type,
            received_timestamp_ms: proto.received_timestamp_ms,
        })
    }
}

#[cfg(feature = "proto")]
impl crate::ProtoSerialize for PriceEntry {
    fn to_proto_bytes(&self) -> Vec<u8> {
        let proto = self.to_proto();
        let mut buf = Vec::new();
        proto.encode_raw(&mut buf);
        buf
    }
}

#[cfg(feature = "proto")]
impl crate::ProtoDeserialize for PriceEntry {
    fn from_proto_bytes(bytes: &[u8]) -> Result<Self, prost::DecodeError> {
        let proto = crate::schema::PriceEntry::decode(bytes)?;
        Self::from_proto(proto)
    }
}
