//! Domain and protocol types for the Tenbin Rust SDK.
//!
//! This module defines the core data structures used across REST and WebSocket APIs.
//! All types are serializable/deserializable for the wire protocol.
//!
//! ## Key Types
//!
//! - `Order`: Mint/redeem order for submission and retrieval
//! - `DeploymentInfo`: Contract addresses and configuration
//! - `QuoteRequest`/`QuoteResponse`: Price quoting (REST API)
//! - `WsQuoteResponse`/`WsQuoteResponseData`: Price quoting (WebSocket API)
//! - `OrderType`: Mint vs Redeem
//! - `OrderStatus`: Order lifecycle states

use std::str::FromStr;

use alloy::{
    primitives::{Address, B256, U256},
    signers::Signature,
};
use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_with::serde_as;
use uuid::Uuid;

use crate::bindings::controller::IController;

/// Custom serde module for Signature that serializes to/from hex string
mod signature_hex {
    use super::*;

    pub fn serialize<S>(sig: &Option<Signature>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match sig {
            Some(s) => serializer.serialize_str(&s.to_string()),
            None => serializer.serialize_none(),
        }
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Signature>, D::Error>
    where
        D: Deserializer<'de>,
    {
        let opt: Option<String> = Option::deserialize(deserializer)?;
        match opt {
            Some(s) => {
                let sig = Signature::from_str(&s).map_err(serde::de::Error::custom)?;
                Ok(Some(sig))
            }
            None => Ok(None),
        }
    }
}

/// Type of the user order.
///
/// Serialized as a string.
///
/// This enum is part of the external wire protocol and is used across both REST
/// and WebSocket flows.
#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Copy, Debug)]
#[serde(rename_all = "PascalCase")]
pub enum OrderType {
    /// Mint event type
    Mint,
    /// Redeem event type
    Redeem,
}

/// Status of the order in the processing
///
/// Serialized as a string.
///
/// This enum represents the lifecycle state of an order as tracked by the platform.
/// Callers should treat it as authoritative when received from the server.
#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Copy, Debug, Default)]
#[serde(rename_all = "PascalCase")]
pub enum OrderStatus {
    /// Pending order
    #[default]
    Pending,
    /// Processing (sent to RE for execution)
    Processing,
    /// Cancelled / error
    Cancelled,
    /// Expired order
    Expired,
    /// Fulfilled, meaning tokens are minted or redeemed
    Fulfilled,
}

/// User order for minting or redeeming tokens.
///
/// Used for order submission and retrieval. When submitting a new order,
/// the `id`, `order_status`, `created`, and `tx_hash` fields are set by the
/// server and can be omitted or defaulted.
///
/// ## Creating an Order for Submission
///
/// ```ignore
/// use tenbin_sdk::types::{Order, OrderType, OrderStatus};
/// use alloy::primitives::{Address, U256};
///
/// let order = Order {
///     id: Default::default(),           // Server assigns
///     order_type: OrderType::Mint,
///     order_status: OrderStatus::Pending,
///     nonce: U256::from(1u64),           // Unique per payer
///     expiry: 1700000000,                // Unix timestamp
///     payer: my_address,                 // Who pays collateral
///     recipient: my_address,             // Who receives tokens
///     collateral_token: usdc_address,
///     collateral_amount: U256::from(1_000_000u64),
///     asset_amount: U256::from(100u64),
///     signature: None,                   // Set after signing
///     controller: controller_address,   // From deployment info
///     created: 0,                        // Server sets
///     tx_hash: None,                     // Server sets on fulfillment
/// };
/// ```
#[serde_as]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Order {
    /// Server-assigned order ID. Ignored when submitting a new order.
    #[serde(default)]
    pub id: Uuid,
    /// Whether this order mints or redeems tokens.
    pub order_type: OrderType,
    /// Current order status. Defaults to `Pending` for new orders.
    #[serde(default)]
    pub order_status: OrderStatus,
    /// Unique nonce for this payer. Prevents replay attacks.
    pub nonce: U256,
    /// Unix timestamp after which the order is invalid.
    pub expiry: u64,
    /// Address paying the collateral (must sign the order).
    pub payer: Address,
    /// Address receiving the minted/redeemed tokens.
    pub recipient: Address,
    /// Collateral token contract address (e.g., USDC).
    pub collateral_token: Address,
    /// Amount of collateral tokens (in token's smallest unit).
    #[serde_as(as = "serde_with::DisplayFromStr")]
    pub collateral_amount: U256,
    /// Amount of asset tokens to mint/redeem.
    #[serde_as(as = "serde_with::DisplayFromStr")]
    pub asset_amount: U256,
    /// EIP-712 signature from the payer. Required for submission.
    /// Serialized as hex string (e.g., "0x1234...") for wire compatibility.
    /// Not included in server responses (only used for submission).
    #[serde(with = "signature_hex", default)]
    pub signature: Option<Signature>,
    /// Controller contract address. Obtain from [`DeploymentInfo`].
    pub controller: Address,
    /// Server-assigned creation timestamp. Ignored for submission.
    #[serde(default)]
    pub created: u64,
    /// Transaction hash when fulfilled. Only present for completed orders.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tx_hash: Option<String>,
}

/// Contract deployment information.
///
/// Returned by the `/deployment` endpoint and contains the addresses
/// for key contracts in the deployment.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DeploymentInfo {
    /// Controller contract address
    pub controller: Address,
    /// EIP-712 domain separator for order signing
    pub domain_separator: B256,
    /// Collateral token contract address
    pub collateral: Address,
    /// Asset token contract address
    pub token: Address,
}

/// Side of the quote (mint or redeem).
///
/// Serialized as a string matching the API format.
#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Copy, Debug)]
#[serde(rename_all = "PascalCase")]
pub enum QuoteSide {
    /// Mint tokens from collateral
    Mint,
    /// Redeem tokens for collateral
    Redeem,
}

/// Quote request payload.
///
/// Submitted to the `/quote` endpoint to request a price quote
/// for a mint or redeem operation.
#[serde_as]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QuoteRequest {
    /// Side of the operation (mint or redeem)
    pub side: QuoteSide,
    /// Size of the operation
    #[serde_as(as = "serde_with::DisplayFromStr")]
    pub size: U256,
}

/// Status of a quote response.
#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Copy, Debug)]
pub enum QuoteStatus {
    /// Quote was successful
    Ok,
    /// Quote failed
    Failed,
}

/// Quote response payload.
///
/// Returned by the `/quote` endpoint with pricing information.
#[serde_as]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QuoteResponse {
    /// Quote identifier
    pub id: Uuid,
    /// Status of the quote
    pub status: QuoteStatus,
    /// USD price (present if status is Ok)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub usd_price: Option<Decimal>,
    /// USDC collateral amount (present if status is Ok)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
    pub usdc_collateral: Option<U256>,
}

/// WebSocket quote response data payload.
///
/// Contains the pricing details returned when a WebSocket quote request succeeds.
/// This is the inner `data` field of [`WsQuoteResponse`] and is only present
/// when the quote status is [`QuoteStatus::Ok`].
#[serde_as]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WsQuoteResponseData {
    /// USD price for the requested operation
    pub usd_price: Decimal,
    /// USDC collateral amount required (serialized as string)
    #[serde_as(as = "serde_with::DisplayFromStr")]
    pub usdc_collateral: U256,
}

/// WebSocket quote response payload.
///
/// Returned via WebSocket in response to a [`QuoteRequest`] message.
/// Unlike the REST [`QuoteResponse`], the WebSocket variant wraps pricing
/// details in a nested `data` field that is only present when the quote succeeds.
///
/// ## Response Structure
///
/// When `status` is [`QuoteStatus::Ok`]:
/// ```json
/// {
///   "id": "uuid",
///   "status": "Ok",
///   "data": {
///     "usd_price": "123.45",
///     "usdc_collateral": "1000000"
///   }
/// }
/// ```
///
/// When `status` is [`QuoteStatus::Failed`]:
/// ```json
/// {
///   "id": "uuid",
///   "status": "Failed"
/// }
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WsQuoteResponse {
    /// Quote identifier
    pub id: Uuid,
    /// Status of the quote
    pub status: QuoteStatus,
    /// Pricing data (present only when status is Ok)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub data: Option<WsQuoteResponseData>,
}

impl From<&Order> for IController::Order {
    fn from(order: &Order) -> Self {
        let nonce = order.nonce;
        let expiry = U256::from(order.expiry);
        let payer = order.payer;
        let recipient = order.recipient;
        let collateral_token = order.collateral_token;
        let collateral_amount = order.collateral_amount;
        let asset_amount = order.asset_amount;
        IController::Order {
            order_type: order.order_type as u8,
            nonce,
            expiry,
            payer,
            recipient,
            collateral_token,
            collateral_amount,
            asset_amount,
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::fixtures::valid_order;

    use super::*;
    use serde_json;

    #[test]
    fn all_order_types_serialize_and_deserialize() {
        let variants = vec![OrderType::Mint, OrderType::Redeem];

        for v in variants {
            let json = serde_json::to_string(&v).expect("serialize");
            let back: OrderType = serde_json::from_str(&json).expect("deserialize");
            assert_eq!(v as u8, back as u8);
        }
    }

    #[test]
    fn order_roundtrip_and_controller_conversion() {
        let order = valid_order();

        // Serde coverage
        let json = serde_json::to_string(&order).unwrap();
        let decoded: Order = serde_json::from_str(&json).unwrap();
        assert_eq!(decoded.nonce, order.nonce);

        // Conversion coverage
        let controller_order = IController::Order::from(&order);

        match controller_order {
            IController::Order {
                order_type,
                nonce,
                expiry,
                payer,
                recipient,
                collateral_token,
                collateral_amount,
                asset_amount,
            } => {
                assert_eq!(order_type, OrderType::Mint as u8);
                assert_eq!(nonce, 1);
                assert_eq!(expiry, order.expiry);
                assert_eq!(payer, order.payer);
                assert_eq!(recipient, order.recipient);
                assert_eq!(collateral_token, order.collateral_token);
                assert_eq!(collateral_amount, order.collateral_amount);
                assert_eq!(asset_amount, order.asset_amount);
            }
        }
    }

    #[test]
    fn ws_quote_response_ok_roundtrip() {
        let response = WsQuoteResponse {
            id: Uuid::nil(),
            status: QuoteStatus::Ok,
            data: Some(WsQuoteResponseData {
                usd_price: Decimal::new(12345, 2), // 123.45
                usdc_collateral: U256::from(1_000_000u64),
            }),
        };

        let json = serde_json::to_string(&response).unwrap();
        let decoded: WsQuoteResponse = serde_json::from_str(&json).unwrap();

        assert_eq!(decoded.id, response.id);
        assert_eq!(decoded.status, QuoteStatus::Ok);
        assert!(decoded.data.is_some());

        let data = decoded.data.unwrap();
        assert_eq!(data.usd_price, Decimal::new(12345, 2));
        assert_eq!(data.usdc_collateral, U256::from(1_000_000u64));
    }

    #[test]
    fn ws_quote_response_failed_roundtrip() {
        let response = WsQuoteResponse {
            id: Uuid::nil(),
            status: QuoteStatus::Failed,
            data: None,
        };

        let json = serde_json::to_string(&response).unwrap();
        let decoded: WsQuoteResponse = serde_json::from_str(&json).unwrap();

        assert_eq!(decoded.id, response.id);
        assert_eq!(decoded.status, QuoteStatus::Failed);
        assert!(decoded.data.is_none());

        // Verify the JSON doesn't include the data field when None
        assert!(!json.contains("data"));
    }

    #[test]
    fn ws_quote_response_data_roundtrip() {
        let data = WsQuoteResponseData {
            usd_price: Decimal::new(99999, 3), // 99.999
            usdc_collateral: U256::from(500_000u64),
        };

        let json = serde_json::to_string(&data).unwrap();
        let decoded: WsQuoteResponseData = serde_json::from_str(&json).unwrap();

        assert_eq!(decoded.usd_price, data.usd_price);
        assert_eq!(decoded.usdc_collateral, data.usdc_collateral);
    }

    #[test]
    fn quote_request_roundtrip() {
        let request = QuoteRequest {
            side: QuoteSide::Mint,
            size: U256::from(100u64),
        };

        let json = serde_json::to_string(&request).unwrap();
        let decoded: QuoteRequest = serde_json::from_str(&json).unwrap();

        assert_eq!(decoded.side, QuoteSide::Mint);
        assert_eq!(decoded.size, U256::from(100u64));
    }

    #[test]
    fn quote_side_serialization() {
        assert_eq!(serde_json::to_string(&QuoteSide::Mint).unwrap(), "\"Mint\"");
        assert_eq!(
            serde_json::to_string(&QuoteSide::Redeem).unwrap(),
            "\"Redeem\""
        );
    }

    #[test]
    fn quote_status_serialization() {
        assert_eq!(serde_json::to_string(&QuoteStatus::Ok).unwrap(), "\"Ok\"");
        assert_eq!(
            serde_json::to_string(&QuoteStatus::Failed).unwrap(),
            "\"Failed\""
        );
    }
}
