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

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

#[derive(Debug, Clone, PartialEq)]
#[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 OrderbookEntry {
    pub source: String,
    pub instrument_type: InstrumentType,
    pub pair: Pair,
    pub r#type: OrderbookUpdateType,
    pub data: OrderbookData,
    pub timestamp_ms: i64,
    pub received_timestamp_ms: i64,
}

#[derive(Debug, Clone, PartialEq)]
#[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 enum OrderbookUpdateType {
    Update(UpdateType),
    Snapshot,
}

#[derive(Debug, Clone, PartialEq)]
#[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 enum UpdateType {
    Target,
    Delta,
}

impl std::fmt::Display for UpdateType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Target => write!(f, "target"),
            Self::Delta => write!(f, "delta"),
        }
    }
}

impl std::fmt::Display for OrderbookUpdateType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Update(update_type) => write!(f, "update with type {update_type}"),
            Self::Snapshot => write!(f, "snapshot"),
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
#[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 OrderbookData {
    pub update_id: u64,
    pub bids: Vec<(f64, f64)>,
    pub asks: Vec<(f64, f64)>,
}

#[cfg(feature = "proto")]
impl OrderbookEntry {
    fn to_proto(&self) -> crate::schema::OrderbookEntry {
        crate::schema::OrderbookEntry {
            source: self.source.clone(),
            instrument_type: match self.instrument_type {
                InstrumentType::Spot => crate::schema::InstrumentType::Spot as i32,
                InstrumentType::Perp => crate::schema::InstrumentType::Perp as i32,
            },
            pair: Some(crate::schema::Pair {
                base: self.pair.base.clone(),
                quote: self.pair.quote.clone(),
            }),
            r#type: Some(match &self.r#type {
                OrderbookUpdateType::Update(update_type) => crate::schema::OrderbookUpdateType {
                    update_type: Some(crate::schema::orderbook_update_type::UpdateType::Update(
                        match update_type {
                            UpdateType::Target => crate::schema::UpdateType::Target as i32,
                            UpdateType::Delta => crate::schema::UpdateType::Delta as i32,
                        },
                    )),
                },
                OrderbookUpdateType::Snapshot => crate::schema::OrderbookUpdateType {
                    update_type: Some(crate::schema::orderbook_update_type::UpdateType::Snapshot(
                        true,
                    )),
                },
            }),
            data: Some(crate::schema::OrderbookData {
                update_id: self.data.update_id,
                bids: self
                    .data
                    .bids
                    .iter()
                    .map(|(price, quantity)| crate::schema::BidOrAsk {
                        price: *price,
                        quantity: *quantity,
                    })
                    .collect(),
                asks: self
                    .data
                    .asks
                    .iter()
                    .map(|(price, quantity)| crate::schema::BidOrAsk {
                        price: *price,
                        quantity: *quantity,
                    })
                    .collect(),
            }),
            timestamp_ms: self.timestamp_ms,
            received_timestamp_ms: self.received_timestamp_ms,
        }
    }

    fn from_proto(proto: crate::schema::OrderbookEntry) -> Result<Self, prost::DecodeError> {
        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,
            _ => {
                return Err(prost::DecodeError::new(format!(
                    "Invalid instrument_type value: {}",
                    proto.instrument_type,
                )))
            }
        };

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

        let r#type = match proto.r#type {
            Some(orderbook_update_type) => match orderbook_update_type.update_type {
                Some(crate::schema::orderbook_update_type::UpdateType::Update(
                    update_type_value,
                )) => {
                    let update_type = match update_type_value {
                        x if x == crate::schema::UpdateType::Target as i32 => UpdateType::Target,
                        x if x == crate::schema::UpdateType::Delta as i32 => UpdateType::Delta,
                        _ => {
                            return Err(prost::DecodeError::new(format!(
                                "Invalid update type value: {update_type_value}",
                            )))
                        }
                    };
                    OrderbookUpdateType::Update(update_type)
                }
                Some(crate::schema::orderbook_update_type::UpdateType::Snapshot(_)) => {
                    OrderbookUpdateType::Snapshot
                }
                None => {
                    return Err(prost::DecodeError::new(
                        "Missing update_type field in OrderbookUpdateType".to_string(),
                    ))
                }
            },
            None => {
                return Err(prost::DecodeError::new(
                    "Missing type field in OrderbookEntry".to_string(),
                ))
            }
        };

        let data = proto
            .data
            .ok_or_else(|| prost::DecodeError::new("Missing data field in OrderbookEntry"))?;
        let bids = data
            .bids
            .iter()
            .map(|bid| (bid.price, bid.quantity))
            .collect();
        let asks = data
            .asks
            .iter()
            .map(|ask| (ask.price, ask.quantity))
            .collect();
        let data = OrderbookData {
            update_id: data.update_id,
            bids,
            asks,
        };

        Ok(OrderbookEntry {
            source: proto.source,
            instrument_type,
            pair,
            r#type,
            data,
            timestamp_ms: proto.timestamp_ms,
            received_timestamp_ms: proto.received_timestamp_ms,
        })
    }
}

#[cfg(feature = "proto")]
impl crate::ProtoSerialize for OrderbookEntry {
    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 OrderbookEntry {
    fn from_proto_bytes(bytes: &[u8]) -> Result<Self, prost::DecodeError> {
        let proto = crate::schema::OrderbookEntry::decode(bytes)?;
        Self::from_proto(proto)
    }
}
