use std::fmt;

use serde::Deserialize;

#[derive(Deserialize, Debug, Clone)]
pub struct RestingOrder {
    pub oid: u64,
}

#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct FilledOrder {
    pub total_sz: String,
    pub avg_px: String,
    pub oid: u64,
}

/// Structured exchange error parsed from the error string returned by the API.
///
/// Order and cancel errors are usually returned as a vector with the same length as the
/// batched request. Some pre-validation errors return a single error for the entire batch.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExchangeApiError {
    // -- Order errors --
    /// Price must be divisible by tick size.
    Tick(String),
    /// Order must have minimum value of $10.
    MinTradeNtl(String),
    /// Order must have minimum value of 10 {quote_token}. (spot)
    MinTradeSpotNtl(String),
    /// Insufficient margin to place order.
    InsufficientMargin(String),
    /// Reduce only order would increase position.
    ReduceOnly(String),
    /// Post only order would have immediately matched.
    PostOnlyWouldCross(String),
    /// Order could not immediately match against any resting orders.
    IocCancel(String),
    /// Invalid TP/SL price.
    BadTriggerPx(String),
    /// No liquidity available for market order.
    MarketOrderNoLiquidity(String),
    /// Order would increase open interest while open interest is capped.
    PositionIncreaseAtOpenInterestCap(String),
    /// Order would flip position and increase open interest while OI is capped.
    PositionFlipAtOpenInterestCap(String),
    /// Order rejected due to price more aggressive than oracle while at open interest cap.
    TooAggressiveAtOpenInterestCap(String),
    /// Order would increase open interest too quickly.
    OpenInterestIncrease(String),
    /// (Spot-only) Order has insufficient spot balance to trade.
    InsufficientSpotBalance(String),
    /// Order price too far from oracle.
    Oracle(String),
    /// Order would cause position to exceed margin tier limit at current leverage.
    PerpMaxPosition(String),

    // -- Cancel errors --
    /// Order was never placed, already canceled, or filled.
    MissingOrder(String),

    /// An error not covered by the known variants. Contains the raw error string.
    Other(String),
}

impl ExchangeApiError {
    pub fn parse(s: String) -> Self {
        // Hot path — these three cover the vast majority of exchange errors
        if s.contains("ost only") {
            return Self::PostOnlyWouldCross(s);
        }
        if s.contains("never placed") || s.contains("already cancel") {
            return Self::MissingOrder(s);
        }

        // Everything else
        if s.contains("tick size") {
            Self::Tick(s)
        } else if s.contains("minimum value") {
            if s.contains('$') {
                Self::MinTradeNtl(s)
            } else {
                Self::MinTradeSpotNtl(s)
            }
        } else if s.contains("nsufficient margin") {
            Self::InsufficientMargin(s)
        } else if s.contains("educe only") {
            Self::ReduceOnly(s)
        } else if s.contains("not immediately match") {
            Self::IocCancel(s)
        } else if s.contains("TP/SL") || s.contains("rigger") && s.contains("nvalid") {
            Self::BadTriggerPx(s)
        } else if s.contains("o liquidity") {
            Self::MarketOrderNoLiquidity(s)
        } else if s.contains("open interest") {
            if s.contains("aggressive") {
                Self::TooAggressiveAtOpenInterestCap(s)
            } else if s.contains("flip") {
                Self::PositionFlipAtOpenInterestCap(s)
            } else if s.contains("cap") {
                Self::PositionIncreaseAtOpenInterestCap(s)
            } else {
                Self::OpenInterestIncrease(s)
            }
        } else if s.contains("nsufficient spot balance") {
            Self::InsufficientSpotBalance(s)
        } else if s.contains("far from oracle") {
            Self::Oracle(s)
        } else if s.contains("margin tier") || s.contains("ax position") {
            Self::PerpMaxPosition(s)
        } else {
            Self::Other(s)
        }
    }

    /// Returns the raw error string from the API.
    pub fn raw(&self) -> &str {
        match self {
            Self::Tick(s)
            | Self::MinTradeNtl(s)
            | Self::MinTradeSpotNtl(s)
            | Self::InsufficientMargin(s)
            | Self::ReduceOnly(s)
            | Self::PostOnlyWouldCross(s)
            | Self::IocCancel(s)
            | Self::BadTriggerPx(s)
            | Self::MarketOrderNoLiquidity(s)
            | Self::PositionIncreaseAtOpenInterestCap(s)
            | Self::PositionFlipAtOpenInterestCap(s)
            | Self::TooAggressiveAtOpenInterestCap(s)
            | Self::OpenInterestIncrease(s)
            | Self::InsufficientSpotBalance(s)
            | Self::Oracle(s)
            | Self::PerpMaxPosition(s)
            | Self::MissingOrder(s)
            | Self::Other(s) => s,
        }
    }
}

impl fmt::Display for ExchangeApiError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.raw())
    }
}

impl<'de> Deserialize<'de> for ExchangeApiError {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        Ok(ExchangeApiError::parse(s))
    }
}

#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub enum ExchangeDataStatus {
    Success,
    WaitingForFill,
    WaitingForTrigger,
    Error(ExchangeApiError),
    Resting(RestingOrder),
    Filled(FilledOrder),
}

#[derive(Deserialize, Debug, Clone)]
pub struct ExchangeDataStatuses {
    pub statuses: Vec<ExchangeDataStatus>,
}

#[derive(Deserialize, Debug, Clone)]
pub struct ExchangeResponse {
    #[serde(rename = "type")]
    pub response_type: String,
    pub data: Option<ExchangeDataStatuses>,
}

#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "status", content = "response")]
pub enum ExchangeResponseStatus {
    Ok(ExchangeResponse),
    Err(ExchangeApiError),
}

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

    #[test]
    fn test_parse_documented_errors() {
        let cases: Vec<(&str, fn(&ExchangeApiError) -> bool)> = vec![
            (
                "Price must be divisible by tick size.",
                |e| matches!(e, ExchangeApiError::Tick(_)),
            ),
            (
                "Order must have minimum value of $10.",
                |e| matches!(e, ExchangeApiError::MinTradeNtl(_)),
            ),
            (
                "Order must have minimum value of 10 USDC.",
                |e| matches!(e, ExchangeApiError::MinTradeSpotNtl(_)),
            ),
            (
                "Insufficient margin to place order.",
                |e| matches!(e, ExchangeApiError::InsufficientMargin(_)),
            ),
            (
                "Reduce only order would increase position.",
                |e| matches!(e, ExchangeApiError::ReduceOnly(_)),
            ),
            (
                "Post only order would have immediately matched, bbo was 1234.5.",
                |e| matches!(e, ExchangeApiError::PostOnlyWouldCross(_)),
            ),
            (
                "Order could not immediately match against any resting orders.",
                |e| matches!(e, ExchangeApiError::IocCancel(_)),
            ),
            (
                "Invalid TP/SL price.",
                |e| matches!(e, ExchangeApiError::BadTriggerPx(_)),
            ),
            (
                "No liquidity available for market order.",
                |e| matches!(e, ExchangeApiError::MarketOrderNoLiquidity(_)),
            ),
            (
                "Order would increase open interest while open interest is capped",
                |e| matches!(e, ExchangeApiError::PositionIncreaseAtOpenInterestCap(_)),
            ),
            (
                "Order would flip and increase open interest while open interest is capped",
                |e| matches!(e, ExchangeApiError::PositionFlipAtOpenInterestCap(_)),
            ),
            (
                "Order rejected due to price more aggressive than oracle while at open interest cap",
                |e| matches!(e, ExchangeApiError::TooAggressiveAtOpenInterestCap(_)),
            ),
            (
                "Order would increase open interest too quickly",
                |e| matches!(e, ExchangeApiError::OpenInterestIncrease(_)),
            ),
            (
                "Order has insufficient spot balance to trade",
                |e| matches!(e, ExchangeApiError::InsufficientSpotBalance(_)),
            ),
            (
                "Order price too far from oracle",
                |e| matches!(e, ExchangeApiError::Oracle(_)),
            ),
            (
                "Order would cause position to exceed margin tier limit at current leverage",
                |e| matches!(e, ExchangeApiError::PerpMaxPosition(_)),
            ),
            (
                "Order was never placed, already canceled, or filled.",
                |e| matches!(e, ExchangeApiError::MissingOrder(_)),
            ),
            (
                "something completely unknown",
                |e| matches!(e, ExchangeApiError::Other(_)),
            ),
        ];

        for (input, check) in cases {
            let parsed = ExchangeApiError::parse(input.to_string());
            assert!(
                check(&parsed),
                "Failed to parse: {input:?} -> got {parsed:?}"
            );
            assert_eq!(parsed.raw(), input, "raw() should preserve original string");
        }
    }

    #[test]
    fn test_deserialize_error_status() {
        let json = r#"{"error": "Reduce only order would increase position."}"#;
        let status: ExchangeDataStatus = serde_json::from_str(json).unwrap();
        assert!(matches!(
            status,
            ExchangeDataStatus::Error(ExchangeApiError::ReduceOnly(_))
        ));
    }

    #[test]
    fn test_deserialize_exchange_response_status_err() {
        let json = r#"{"status": "err", "response": "Post only order would have immediately matched, bbo was 100.0."}"#;
        let status: ExchangeResponseStatus = serde_json::from_str(json).unwrap();
        assert!(matches!(
            status,
            ExchangeResponseStatus::Err(ExchangeApiError::PostOnlyWouldCross(_))
        ));
    }
}
