// -------------------------------------------------------------------------------------------------
//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
//  https://nautechsystems.io
//
//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// -------------------------------------------------------------------------------------------------

//! WebSocket message types for Bybit public and private channels.

use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use ustr::Ustr;

use crate::{
    common::{
        enums::{
            BybitCancelType, BybitCreateType, BybitExecType, BybitMarketUnit, BybitOrderSide,
            BybitOrderStatus, BybitOrderType, BybitPositionIdx, BybitPositionSide,
            BybitPositionStatus, BybitProductType, BybitSmpType, BybitStopOrderType,
            BybitTimeInForce, BybitTpSlMode, BybitTriggerDirection, BybitTriggerType,
            BybitWsOrderRequestOp,
        },
        parse::{
            deserialize_decimal_or_zero, deserialize_optional_decimal_or_zero,
            deserialize_optional_decimal_str,
        },
    },
    websocket::enums::BybitWsOperation,
};

/// Bybit WebSocket subscription message.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BybitSubscription {
    pub op: BybitWsOperation,
    pub args: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub req_id: Option<String>,
}

/// Bybit WebSocket authentication message.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BybitAuthRequest {
    pub op: BybitWsOperation,
    pub args: Vec<serde_json::Value>,
}

/// Wire-level frame deserialized from a Bybit WebSocket message.
///
/// Represents the raw protocol layer before the handler converts data
/// variants into the public [`BybitWsMessage`] API.
#[derive(Debug, Clone)]
pub enum BybitWsFrame {
    /// Authentication acknowledgement.
    Auth(BybitWsAuthResponse),
    /// Subscription acknowledgement.
    Subscription(BybitWsSubscriptionMsg),
    /// Order operation response (create/amend/cancel) from trade WebSocket.
    OrderResponse(BybitWsOrderResponse),
    /// Error response from the venue.
    ErrorResponse(BybitWsResponse),
    /// Orderbook snapshot or delta.
    Orderbook(BybitWsOrderbookDepthMsg),
    /// Trade updates.
    Trade(BybitWsTradeMsg),
    /// Kline updates.
    Kline(BybitWsKlineMsg),
    /// Linear/inverse ticker update.
    TickerLinear(BybitWsTickerLinearMsg),
    /// Option ticker update.
    TickerOption(BybitWsTickerOptionMsg),
    /// Order updates from private channel.
    AccountOrder(BybitWsAccountOrderMsg),
    /// Execution/fill updates from private channel.
    AccountExecution(BybitWsAccountExecutionMsg),
    /// Wallet/balance updates from private channel.
    AccountWallet(BybitWsAccountWalletMsg),
    /// Position updates from private channel.
    AccountPosition(BybitWsAccountPositionMsg),
    /// Payload that does not match any known frame type.
    Unknown(Value),
    /// Notification that the underlying connection reconnected.
    Reconnected,
}

/// High-level message emitted by the Bybit WebSocket client.
#[derive(Debug, Clone)]
pub enum BybitWsMessage {
    /// Authentication acknowledgement.
    Auth(BybitWsAuthResponse),
    /// Order operation response (create/amend/cancel) from trade WebSocket.
    OrderResponse(BybitWsOrderResponse),
    /// Orderbook snapshot or delta.
    Orderbook(BybitWsOrderbookDepthMsg),
    /// Trade updates.
    Trade(BybitWsTradeMsg),
    /// Kline updates.
    Kline(BybitWsKlineMsg),
    /// Linear/inverse ticker update.
    TickerLinear(BybitWsTickerLinearMsg),
    /// Option ticker update.
    TickerOption(BybitWsTickerOptionMsg),
    /// Order updates from private channel.
    AccountOrder(BybitWsAccountOrderMsg),
    /// Execution/fill updates from private channel.
    AccountExecution(BybitWsAccountExecutionMsg),
    /// Wallet/balance updates from private channel.
    AccountWallet(BybitWsAccountWalletMsg),
    /// Position updates from private channel.
    AccountPosition(BybitWsAccountPositionMsg),
    /// Error received from the venue or client lifecycle.
    Error(BybitWebSocketError),
    /// Notification that the underlying connection reconnected.
    Reconnected,
}

/// Represents an error event surfaced by the WebSocket client.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
#[cfg_attr(
    feature = "python",
    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
)]
pub struct BybitWebSocketError {
    /// Error/return code reported by Bybit.
    pub code: i64,
    /// Human readable message.
    pub message: String,
    /// Optional connection identifier.
    #[serde(default)]
    pub conn_id: Option<String>,
    /// Optional topic associated with the error (when applicable).
    #[serde(default)]
    pub topic: Option<String>,
    /// Optional request identifier related to the failure.
    #[serde(default)]
    pub req_id: Option<String>,
}

impl BybitWebSocketError {
    /// Creates a new error with the provided code/message.
    #[must_use]
    pub fn new(code: i64, message: impl Into<String>) -> Self {
        Self {
            code,
            message: message.into(),
            conn_id: None,
            topic: None,
            req_id: None,
        }
    }

    /// Builds an error payload from a generic response frame.
    #[must_use]
    pub fn from_response(response: &BybitWsResponse) -> Self {
        // Build a more informative error message when ret_msg is missing
        let message = response.ret_msg.clone().unwrap_or_else(|| {
            let mut parts = vec![];

            if let Some(op) = &response.op {
                parts.push(format!("op={op}"));
            }

            if let Some(topic) = &response.topic {
                parts.push(format!("topic={topic}"));
            }

            if let Some(success) = response.success {
                parts.push(format!("success={success}"));
            }

            if parts.is_empty() {
                "Bybit websocket error (no error message provided)".to_string()
            } else {
                format!("Bybit websocket error: {}", parts.join(", "))
            }
        });

        Self {
            code: response.ret_code.unwrap_or_default(),
            message,
            conn_id: response.conn_id.clone(),
            topic: response.topic.map(|t| t.to_string()),
            req_id: response.req_id.clone(),
        }
    }

    /// Convenience constructor for client-side errors (e.g. parsing failures).
    #[must_use]
    pub fn from_message(message: impl Into<String>) -> Self {
        Self::new(-1, message)
    }
}

/// Generic WebSocket request for Bybit trading commands.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsRequest<T> {
    /// Request ID for correlation (will be echoed back in response).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub req_id: Option<String>,
    /// Operation type (order.create, order.amend, order.cancel, etc.).
    pub op: BybitWsOrderRequestOp,
    /// Request header containing timestamp and other metadata.
    pub header: BybitWsHeader,
    /// Arguments payload for the operation.
    pub args: Vec<T>,
}

/// Header for WebSocket trade requests.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub struct BybitWsHeader {
    /// Timestamp in milliseconds.
    pub x_bapi_timestamp: String,
    /// Optional referer ID.
    #[serde(rename = "Referer", skip_serializing_if = "Option::is_none")]
    pub referer: Option<String>,
}

impl BybitWsHeader {
    /// Creates a new header with the current timestamp.
    #[must_use]
    pub fn now() -> Self {
        Self::with_referer(None)
    }

    /// Creates a new header with the current timestamp and optional referer.
    #[must_use]
    pub fn with_referer(referer: Option<String>) -> Self {
        use nautilus_core::time::get_atomic_clock_realtime;
        Self {
            x_bapi_timestamp: get_atomic_clock_realtime().get_time_ms().to_string(),
            referer,
        }
    }
}

/// Parameters for placing an order via WebSocket.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsPlaceOrderParams {
    pub category: BybitProductType,
    pub symbol: Ustr,
    pub side: BybitOrderSide,
    pub order_type: BybitOrderType,
    pub qty: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_leverage: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub market_unit: Option<BybitMarketUnit>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub price: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub time_in_force: Option<BybitTimeInForce>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub order_link_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reduce_only: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub close_on_trigger: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub trigger_price: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub trigger_by: Option<BybitTriggerType>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub trigger_direction: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tpsl_mode: Option<BybitTpSlMode>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub take_profit: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stop_loss: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tp_trigger_by: Option<BybitTriggerType>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sl_trigger_by: Option<BybitTriggerType>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sl_trigger_price: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tp_trigger_price: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sl_order_type: Option<BybitOrderType>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tp_order_type: Option<BybitOrderType>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sl_limit_price: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tp_limit_price: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub order_iv: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mmp: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub position_idx: Option<BybitPositionIdx>,
}

/// Parameters for amending an order via WebSocket.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAmendOrderParams {
    pub category: BybitProductType,
    pub symbol: Ustr,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub order_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub order_link_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub qty: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub price: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub trigger_price: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub take_profit: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stop_loss: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tp_trigger_by: Option<BybitTriggerType>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sl_trigger_by: Option<BybitTriggerType>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub order_iv: Option<String>,
}

/// Parameters for canceling an order via WebSocket.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsCancelOrderParams {
    pub category: BybitProductType,
    pub symbol: Ustr,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub order_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub order_link_id: Option<String>,
}

/// Item in a batch cancel request (without category field).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsBatchCancelItem {
    pub symbol: Ustr,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub order_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub order_link_id: Option<String>,
}

/// Arguments for batch cancel order operation via WebSocket.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BybitWsBatchCancelOrderArgs {
    pub category: BybitProductType,
    pub request: Vec<BybitWsBatchCancelItem>,
}

/// Item in a batch place request (same as BybitWsPlaceOrderParams but without category).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsBatchPlaceItem {
    pub symbol: Ustr,
    pub side: BybitOrderSide,
    pub order_type: BybitOrderType,
    pub qty: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_leverage: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub market_unit: Option<BybitMarketUnit>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub price: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub time_in_force: Option<BybitTimeInForce>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub order_link_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reduce_only: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub close_on_trigger: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub trigger_price: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub trigger_by: Option<BybitTriggerType>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub trigger_direction: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tpsl_mode: Option<BybitTpSlMode>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub take_profit: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stop_loss: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tp_trigger_by: Option<BybitTriggerType>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sl_trigger_by: Option<BybitTriggerType>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sl_trigger_price: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tp_trigger_price: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sl_order_type: Option<BybitOrderType>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tp_order_type: Option<BybitOrderType>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sl_limit_price: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tp_limit_price: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub order_iv: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mmp: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub position_idx: Option<BybitPositionIdx>,
}

/// Arguments for batch place order operation via WebSocket.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BybitWsBatchPlaceOrderArgs {
    pub category: BybitProductType,
    pub request: Vec<BybitWsBatchPlaceItem>,
}

/// Subscription acknowledgement returned by Bybit.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BybitWsSubscriptionMsg {
    pub success: bool,
    pub op: BybitWsOperation,
    #[serde(default)]
    pub conn_id: Option<String>,
    #[serde(default)]
    pub req_id: Option<String>,
    #[serde(default)]
    pub ret_msg: Option<String>,
}

/// Generic response returned by the endpoint when subscribing or authenticating.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BybitWsResponse {
    #[serde(default)]
    pub op: Option<BybitWsOperation>,
    #[serde(default)]
    pub topic: Option<Ustr>,
    #[serde(default)]
    pub success: Option<bool>,
    #[serde(default)]
    pub conn_id: Option<String>,
    #[serde(default)]
    pub req_id: Option<String>,
    #[serde(default)]
    pub ret_code: Option<i64>,
    #[serde(default)]
    pub ret_msg: Option<String>,
}

/// Order operation response from WebSocket trade API.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsOrderResponse {
    /// Operation type (order.create, order.amend, order.cancel).
    pub op: Ustr,
    /// Connection ID.
    #[serde(default)]
    pub conn_id: Option<String>,
    /// Return code (0 = success, non-zero = error).
    pub ret_code: i64,
    /// Return message.
    pub ret_msg: String,
    /// Response data (usually empty for errors, may contain order details for success).
    #[serde(default)]
    pub data: Value,
    /// Request ID for correlation (echoed back if provided in request).
    #[serde(default)]
    pub req_id: Option<String>,
    /// Request header containing timestamp and rate limit info.
    #[serde(default)]
    pub header: Option<Value>,
    /// Extended info for errors.
    #[serde(default)]
    pub ret_ext_info: Option<Value>,
}

impl BybitWsOrderResponse {
    /// Extracts individual order errors from retExtInfo for batch operations.
    ///
    /// For batch operations, even when ret_code is 0, individual orders may fail.
    /// These failures are reported in retExtInfo.list as an array of {code, msg} objects.
    #[must_use]
    pub fn extract_batch_errors(&self) -> Vec<BybitBatchOrderError> {
        self.ret_ext_info
            .as_ref()
            .and_then(|ext| ext.get("list"))
            .and_then(|list| list.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|item| {
                        let code = item.get("code")?.as_i64()?;
                        let msg = item.get("msg")?.as_str()?.to_string();
                        Some(BybitBatchOrderError { code, msg })
                    })
                    .collect()
            })
            .unwrap_or_default()
    }
}

/// Error information for individual orders in a batch operation.
#[derive(Clone, Debug)]
pub struct BybitBatchOrderError {
    /// Error code (0 = success, non-zero = error).
    pub code: i64,
    /// Error message.
    pub msg: String,
}

/// Authentication acknowledgement for private channels.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAuthResponse {
    pub op: BybitWsOperation,
    #[serde(default)]
    pub conn_id: Option<String>,
    #[serde(default)]
    pub ret_code: Option<i64>,
    #[serde(default)]
    pub ret_msg: Option<String>,
    #[serde(default)]
    pub success: Option<bool>,
}

/// Representation of a kline/candlestick event on the public stream.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsKline {
    pub start: i64,
    pub end: i64,
    pub interval: Ustr,
    pub open: String,
    pub close: String,
    pub high: String,
    pub low: String,
    pub volume: String,
    pub turnover: String,
    pub confirm: bool,
    pub timestamp: i64,
}

/// Envelope for kline updates.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsKlineMsg {
    pub topic: Ustr,
    pub ts: i64,
    #[serde(rename = "type")]
    pub msg_type: Ustr,
    pub data: Vec<BybitWsKline>,
}

/// Orderbook depth payload consisting of raw ladder deltas.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BybitWsOrderbookDepth {
    /// Symbol.
    pub s: Ustr,
    /// Bid levels represented as `[price, size]` string pairs.
    pub b: Vec<Vec<String>>,
    /// Ask levels represented as `[price, size]` string pairs.
    pub a: Vec<Vec<String>>,
    /// Update identifier.
    pub u: i64,
    /// Cross sequence number.
    pub seq: i64,
}

/// Envelope for orderbook depth snapshots and updates.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsOrderbookDepthMsg {
    pub topic: Ustr,
    #[serde(rename = "type")]
    pub msg_type: Ustr,
    pub ts: i64,
    pub data: BybitWsOrderbookDepth,
    #[serde(default)]
    pub cts: Option<i64>,
}

/// Linear/Inverse ticker event payload.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsTickerLinear {
    pub symbol: Ustr,
    #[serde(default)]
    pub tick_direction: Option<String>,
    #[serde(default)]
    pub price24h_pcnt: Option<String>,
    #[serde(default)]
    pub last_price: Option<String>,
    #[serde(default)]
    pub prev_price24h: Option<String>,
    #[serde(default)]
    pub high_price24h: Option<String>,
    #[serde(default)]
    pub low_price24h: Option<String>,
    #[serde(default)]
    pub prev_price1h: Option<String>,
    #[serde(default)]
    pub mark_price: Option<String>,
    #[serde(default)]
    pub index_price: Option<String>,
    #[serde(default)]
    pub open_interest: Option<String>,
    #[serde(default)]
    pub open_interest_value: Option<String>,
    #[serde(default)]
    pub turnover24h: Option<String>,
    #[serde(default)]
    pub volume24h: Option<String>,
    #[serde(default)]
    pub next_funding_time: Option<String>,
    #[serde(default)]
    pub funding_rate: Option<String>,
    #[serde(default)]
    pub bid1_price: Option<String>,
    #[serde(default)]
    pub bid1_size: Option<String>,
    #[serde(default)]
    pub ask1_price: Option<String>,
    #[serde(default)]
    pub ask1_size: Option<String>,
    #[serde(default)]
    pub funding_interval_hour: Option<String>,
}

/// Envelope for linear ticker updates.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsTickerLinearMsg {
    pub topic: Ustr,
    #[serde(rename = "type")]
    pub msg_type: Ustr,
    pub ts: i64,
    #[serde(default)]
    pub cs: Option<i64>,
    pub data: BybitWsTickerLinear,
}

/// Option ticker event payload.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsTickerOption {
    pub symbol: Ustr,
    pub bid_price: String,
    pub bid_size: String,
    pub bid_iv: String,
    pub ask_price: String,
    pub ask_size: String,
    pub ask_iv: String,
    pub last_price: String,
    pub high_price24h: String,
    pub low_price24h: String,
    pub mark_price: String,
    pub index_price: String,
    pub mark_price_iv: String,
    pub underlying_price: String,
    pub open_interest: String,
    pub turnover24h: String,
    pub volume24h: String,
    pub total_volume: String,
    pub total_turnover: String,
    pub delta: String,
    pub gamma: String,
    pub vega: String,
    pub theta: String,
    pub predicted_delivery_price: String,
    pub change24h: String,
}

/// Envelope for option ticker updates.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsTickerOptionMsg {
    #[serde(default)]
    pub id: Option<String>,
    pub topic: Ustr,
    #[serde(rename = "type")]
    pub msg_type: Ustr,
    pub ts: i64,
    pub data: BybitWsTickerOption,
}

/// Trade event payload containing trade executions on public feeds.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BybitWsTrade {
    #[serde(rename = "T")]
    pub t: i64,
    #[serde(rename = "s")]
    pub s: Ustr,
    #[serde(rename = "S")]
    pub taker_side: BybitOrderSide,
    #[serde(rename = "v")]
    pub v: String,
    #[serde(rename = "p")]
    pub p: String,
    #[serde(rename = "i")]
    pub i: String,
    #[serde(rename = "BT")]
    pub bt: bool,
    #[serde(rename = "L")]
    #[serde(default)]
    pub l: Option<String>,
    #[serde(rename = "id")]
    #[serde(default)]
    pub id: Option<Ustr>,
    #[serde(rename = "mP")]
    #[serde(default)]
    pub m_p: Option<String>,
    #[serde(rename = "iP")]
    #[serde(default)]
    pub i_p: Option<String>,
    #[serde(rename = "mIv")]
    #[serde(default)]
    pub m_iv: Option<String>,
    #[serde(rename = "iv")]
    #[serde(default)]
    pub iv: Option<String>,
}

/// Envelope for public trade updates.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsTradeMsg {
    pub topic: Ustr,
    #[serde(rename = "type")]
    pub msg_type: Ustr,
    pub ts: i64,
    pub data: Vec<BybitWsTrade>,
}

/// Private order stream payload.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountOrder {
    pub category: BybitProductType,
    pub symbol: Ustr,
    pub order_id: Ustr,
    pub side: BybitOrderSide,
    pub order_type: BybitOrderType,
    pub cancel_type: BybitCancelType,
    pub price: String,
    pub qty: String,
    pub order_iv: String,
    pub time_in_force: BybitTimeInForce,
    pub order_status: BybitOrderStatus,
    pub order_link_id: Ustr,
    pub last_price_on_created: Ustr,
    pub reduce_only: bool,
    pub leaves_qty: String,
    pub leaves_value: String,
    pub cum_exec_qty: String,
    pub cum_exec_value: String,
    pub avg_price: String,
    pub block_trade_id: Ustr,
    pub position_idx: i32,
    pub cum_exec_fee: String,
    pub created_time: String,
    pub updated_time: String,
    pub reject_reason: Ustr,
    pub trigger_price: String,
    pub take_profit: String,
    pub stop_loss: String,
    pub tp_trigger_by: BybitTriggerType,
    pub sl_trigger_by: BybitTriggerType,
    pub tp_limit_price: String,
    pub sl_limit_price: String,
    pub close_on_trigger: bool,
    pub place_type: Ustr,
    pub smp_type: BybitSmpType,
    pub smp_group: i32,
    pub smp_order_id: Ustr,
    pub fee_currency: Ustr,
    pub trigger_by: BybitTriggerType,
    pub stop_order_type: BybitStopOrderType,
    pub trigger_direction: BybitTriggerDirection,
    #[serde(default)]
    pub tpsl_mode: Option<BybitTpSlMode>,
    #[serde(default)]
    pub create_type: Option<BybitCreateType>,
}

/// Envelope for account order updates.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountOrderMsg {
    pub topic: Ustr,
    pub id: String,
    pub creation_time: i64,
    pub data: Vec<BybitWsAccountOrder>,
}

/// Private execution (fill) stream payload.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountExecution {
    pub category: BybitProductType,
    pub symbol: Ustr,
    pub exec_fee: String,
    pub exec_id: String,
    pub exec_price: String,
    pub exec_qty: String,
    pub exec_type: BybitExecType,
    pub exec_value: String,
    pub is_maker: bool,
    pub fee_rate: String,
    pub trade_iv: String,
    pub mark_iv: String,
    pub block_trade_id: Ustr,
    pub mark_price: String,
    pub index_price: String,
    pub underlying_price: String,
    pub leaves_qty: String,
    pub order_id: Ustr,
    pub order_link_id: Ustr,
    pub order_price: String,
    pub order_qty: String,
    pub order_type: BybitOrderType,
    pub side: BybitOrderSide,
    pub exec_time: String,
    pub is_leverage: String,
    pub closed_size: String,
    pub seq: i64,
    pub stop_order_type: BybitStopOrderType,
}

/// Envelope for account execution updates.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountExecutionMsg {
    pub topic: Ustr,
    pub id: String,
    pub creation_time: i64,
    pub data: Vec<BybitWsAccountExecution>,
}

/// Coin level wallet update payload on private streams.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountWalletCoin {
    pub coin: Ustr,
    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
    pub wallet_balance: Decimal,
    pub available_to_withdraw: String,
    pub available_to_borrow: String,
    pub accrued_interest: String,
    #[serde(
        default,
        rename = "totalOrderIM",
        deserialize_with = "deserialize_optional_decimal_or_zero"
    )]
    pub total_order_im: Decimal,
    #[serde(
        default,
        rename = "totalPositionIM",
        deserialize_with = "deserialize_optional_decimal_or_zero"
    )]
    pub total_position_im: Decimal,
    #[serde(default, rename = "totalPositionMM")]
    pub total_position_mm: Option<String>,
    pub equity: String,
    #[serde(default, deserialize_with = "deserialize_optional_decimal_or_zero")]
    pub spot_borrow: Decimal,
}

/// Wallet summary payload covering all coins.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountWallet {
    pub total_wallet_balance: String,
    pub total_equity: String,
    pub total_available_balance: String,
    pub total_margin_balance: String,
    pub total_initial_margin: String,
    pub total_maintenance_margin: String,
    #[serde(rename = "accountIMRate")]
    pub account_im_rate: String,
    #[serde(rename = "accountMMRate")]
    pub account_mm_rate: String,
    #[serde(rename = "accountLTV")]
    pub account_ltv: String,
    pub coin: Vec<BybitWsAccountWalletCoin>,
}

/// Envelope for wallet updates on private streams.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountWalletMsg {
    pub topic: Ustr,
    pub id: String,
    pub creation_time: i64,
    pub data: Vec<BybitWsAccountWallet>,
}

/// Position data from private position stream.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountPosition {
    pub category: BybitProductType,
    pub symbol: Ustr,
    pub side: BybitPositionSide,
    pub size: String,
    pub position_idx: i32,
    pub trade_mode: i32,
    pub position_value: String,
    pub risk_id: i64,
    pub risk_limit_value: String,
    #[serde(deserialize_with = "deserialize_optional_decimal_str")]
    pub entry_price: Option<Decimal>,
    pub mark_price: String,
    pub leverage: String,
    pub position_balance: String,
    pub auto_add_margin: i32,
    #[serde(rename = "positionIM")]
    pub position_im: String,
    #[serde(rename = "positionIMByMp")]
    pub position_im_by_mp: String,
    #[serde(rename = "positionMM")]
    pub position_mm: String,
    #[serde(rename = "positionMMByMp")]
    pub position_mm_by_mp: String,
    pub liq_price: String,
    pub bust_price: String,
    pub tpsl_mode: BybitTpSlMode,
    pub take_profit: String,
    pub stop_loss: String,
    pub trailing_stop: String,
    pub unrealised_pnl: String,
    pub session_avg_price: String,
    pub cur_realised_pnl: String,
    pub cum_realised_pnl: String,
    pub position_status: BybitPositionStatus,
    pub adl_rank_indicator: i32,
    pub created_time: String,
    pub updated_time: String,
    #[serde(default = "default_ws_position_seq")]
    pub seq: i64,
    #[serde(default)]
    pub is_reduce_only: bool,
    #[serde(default)]
    pub mmr_sys_updated_time: String,
    #[serde(default)]
    pub leverage_sys_updated_time: String,
}

const fn default_ws_position_seq() -> i64 {
    -1
}

/// Envelope for position updates on private streams.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountPositionMsg {
    pub topic: Ustr,
    pub id: String,
    pub creation_time: i64,
    pub data: Vec<BybitWsAccountPosition>,
}

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

    use super::*;
    use crate::common::testing::load_test_json;

    #[rstest]
    fn serialize_place_params_includes_order_iv_when_set() {
        let params = BybitWsPlaceOrderParams {
            category: BybitProductType::Option,
            symbol: Ustr::from("BTC-30JUN25-100000-C"),
            side: BybitOrderSide::Buy,
            order_type: BybitOrderType::Limit,
            qty: "0.1".to_string(),
            is_leverage: None,
            market_unit: None,
            price: Some("500".to_string()),
            time_in_force: Some(BybitTimeInForce::Gtc),
            order_link_id: Some("test-1".to_string()),
            reduce_only: None,
            close_on_trigger: None,
            trigger_price: None,
            trigger_by: None,
            trigger_direction: None,
            tpsl_mode: None,
            take_profit: None,
            stop_loss: None,
            tp_trigger_by: None,
            sl_trigger_by: None,
            sl_trigger_price: None,
            tp_trigger_price: None,
            sl_order_type: None,
            tp_order_type: None,
            sl_limit_price: None,
            tp_limit_price: None,
            order_iv: Some("0.80".to_string()),
            mmp: Some(true),
            position_idx: None,
        };

        let json = serde_json::to_string(&params).unwrap();
        assert!(json.contains("\"orderIv\":\"0.80\""));
        assert!(json.contains("\"mmp\":true"));
    }

    #[rstest]
    fn serialize_place_params_omits_order_iv_when_none() {
        let params = BybitWsPlaceOrderParams {
            category: BybitProductType::Linear,
            symbol: Ustr::from("BTCUSDT"),
            side: BybitOrderSide::Buy,
            order_type: BybitOrderType::Limit,
            qty: "0.01".to_string(),
            is_leverage: None,
            market_unit: None,
            price: Some("50000".to_string()),
            time_in_force: Some(BybitTimeInForce::Gtc),
            order_link_id: None,
            reduce_only: None,
            close_on_trigger: None,
            trigger_price: None,
            trigger_by: None,
            trigger_direction: None,
            tpsl_mode: None,
            take_profit: None,
            stop_loss: None,
            tp_trigger_by: None,
            sl_trigger_by: None,
            sl_trigger_price: None,
            tp_trigger_price: None,
            sl_order_type: None,
            tp_order_type: None,
            sl_limit_price: None,
            tp_limit_price: None,
            order_iv: None,
            mmp: None,
            position_idx: None,
        };

        let json = serde_json::to_string(&params).unwrap();
        assert!(!json.contains("orderIv"));
        assert!(!json.contains("mmp"));
        assert!(!json.contains("positionIdx"));
    }

    #[rstest]
    #[case(BybitPositionIdx::BuyHedge, 1)]
    #[case(BybitPositionIdx::SellHedge, 2)]
    fn serialize_place_params_includes_position_idx_when_set(
        #[case] idx: BybitPositionIdx,
        #[case] expected: i32,
    ) {
        let params = BybitWsPlaceOrderParams {
            category: BybitProductType::Linear,
            symbol: Ustr::from("BTCUSDT"),
            side: BybitOrderSide::Buy,
            order_type: BybitOrderType::Limit,
            qty: "0.01".to_string(),
            is_leverage: None,
            market_unit: None,
            price: Some("50000".to_string()),
            time_in_force: Some(BybitTimeInForce::Gtc),
            order_link_id: None,
            reduce_only: None,
            close_on_trigger: None,
            trigger_price: None,
            trigger_by: None,
            trigger_direction: None,
            tpsl_mode: None,
            take_profit: None,
            stop_loss: None,
            tp_trigger_by: None,
            sl_trigger_by: None,
            sl_trigger_price: None,
            tp_trigger_price: None,
            sl_order_type: None,
            tp_order_type: None,
            sl_limit_price: None,
            tp_limit_price: None,
            order_iv: None,
            mmp: None,
            position_idx: Some(idx),
        };

        let json = serde_json::to_string(&params).unwrap();
        assert!(json.contains(&format!("\"positionIdx\":{expected}")));
    }

    #[rstest]
    #[case(None)]
    #[case(Some(BybitPositionIdx::OneWay))]
    #[case(Some(BybitPositionIdx::BuyHedge))]
    #[case(Some(BybitPositionIdx::SellHedge))]
    fn place_params_position_idx_roundtrip(#[case] idx: Option<BybitPositionIdx>) {
        let params = BybitWsPlaceOrderParams {
            category: BybitProductType::Linear,
            symbol: Ustr::from("BTCUSDT"),
            side: BybitOrderSide::Buy,
            order_type: BybitOrderType::Limit,
            qty: "0.01".to_string(),
            is_leverage: None,
            market_unit: None,
            price: Some("50000".to_string()),
            time_in_force: Some(BybitTimeInForce::Gtc),
            order_link_id: None,
            reduce_only: None,
            close_on_trigger: None,
            trigger_price: None,
            trigger_by: None,
            trigger_direction: None,
            tpsl_mode: None,
            take_profit: None,
            stop_loss: None,
            tp_trigger_by: None,
            sl_trigger_by: None,
            sl_trigger_price: None,
            tp_trigger_price: None,
            sl_order_type: None,
            tp_order_type: None,
            sl_limit_price: None,
            tp_limit_price: None,
            order_iv: None,
            mmp: None,
            position_idx: idx,
        };

        let json = serde_json::to_string(&params).unwrap();
        let decoded: BybitWsPlaceOrderParams = serde_json::from_str(&json).unwrap();
        assert_eq!(decoded.position_idx, idx);
    }

    #[rstest]
    fn serialize_amend_params_includes_order_iv_when_set() {
        let params = BybitWsAmendOrderParams {
            category: BybitProductType::Option,
            symbol: Ustr::from("BTC-30JUN25-100000-C"),
            order_id: None,
            order_link_id: Some("test-1".to_string()),
            qty: None,
            price: None,
            trigger_price: None,
            take_profit: None,
            stop_loss: None,
            tp_trigger_by: None,
            sl_trigger_by: None,
            order_iv: Some("0.90".to_string()),
        };

        let json = serde_json::to_string(&params).unwrap();
        assert!(json.contains("\"orderIv\":\"0.90\""));
    }

    #[rstest]
    fn deserialize_account_order_frame_uses_enums() {
        let json = load_test_json("ws_account_order.json");
        let frame: BybitWsAccountOrderMsg = serde_json::from_str(&json).unwrap();
        let order = &frame.data[0];

        assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
        assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
        assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
        assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
        assert_eq!(order.create_type, Some(BybitCreateType::CreateByUser));
        assert_eq!(order.side, BybitOrderSide::Buy);
    }

    #[rstest]
    fn deserialize_ws_account_position_without_conditional_fields() {
        // Bybit v5 docs mark `isReduceOnly`, `mmrSysUpdatedTime`, `leverageSysUpdatedTime`
        // and `seq` as conditional fields that may be absent from position snapshots,
        // e.g. once a position has been closed through the UI (see issue #3836).
        let json = r#"{
            "topic": "position",
            "id": "1",
            "creationTime": 1697673900000,
            "data": [{
                "category": "linear",
                "symbol": "LTCUSDT",
                "side": "",
                "size": "0",
                "positionIdx": 0,
                "tradeMode": 0,
                "positionValue": "0",
                "riskId": 1,
                "riskLimitValue": "150",
                "entryPrice": "",
                "markPrice": "70.00",
                "leverage": "10",
                "positionBalance": "0",
                "autoAddMargin": 0,
                "positionIM": "0",
                "positionIMByMp": "0",
                "positionMM": "0",
                "positionMMByMp": "0",
                "liqPrice": "",
                "bustPrice": "",
                "tpslMode": "Full",
                "takeProfit": "0",
                "stopLoss": "0",
                "trailingStop": "0",
                "unrealisedPnl": "0",
                "sessionAvgPrice": "0",
                "curRealisedPnl": "0",
                "cumRealisedPnl": "0",
                "positionStatus": "Normal",
                "adlRankIndicator": 0,
                "createdTime": "1676538056258",
                "updatedTime": "1697673600012"
            }]
        }"#;

        let msg: BybitWsAccountPositionMsg = serde_json::from_str(json)
            .expect("Failed to parse WS account position with missing conditional fields");
        let position = &msg.data[0];

        assert!(!position.is_reduce_only);
        assert_eq!(position.seq, -1);
        assert_eq!(position.mmr_sys_updated_time, "");
        assert_eq!(position.leverage_sys_updated_time, "");
    }
}
