// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use serde::{Deserialize, Serialize};
use validator::Validate;

use super::rpc_response_context::RpcResponseContext;

/// Request parameters for the `getTokenAccountBalance` RPC method.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Validate)]
pub struct GetTokenAccountBalanceRequest {
    /// The token account address to query balance (base58-encoded)
    #[validate(length(min = 1, message = "Address cannot be empty"))]
    #[validate(custom(function = crate::validation::validate_pubkey))]
    pub address: String,
}

impl GetTokenAccountBalanceRequest {
    pub fn new(address: String) -> Self {
        Self { address }
    }
}

/// Token account balance information.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TokenAccountBalance {
    /// The raw balance without decimals, a string representation of u64.
    pub amount: String,

    /// Number of base 10 digits to the right of the decimal place.
    pub decimals: u8,

    /// The balance using mint-prescribed decimals.
    ///
    /// **Note: DEPRECATED and may lose precision for large amounts.**
    /// f64 has only 53 bits of precision while token amounts can be u64 (64 bits).
    /// For amounts > 2^53 (~9 quadrillion), this value may be rounded.
    /// Use `ui_amount_string` for precise values.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ui_amount: Option<f64>,

    /// The balance as a string using mint-prescribed decimals.
    /// This is the precise representation - use this instead of `ui_amount`.
    pub ui_amount_string: String,
}

impl TokenAccountBalance {
    /// Create a new token account balance.
    ///
    /// # Arguments
    /// * `amount` - Raw token amount (u64)
    /// * `decimals` - Token's decimal places
    pub fn new(amount: u64, decimals: u8) -> Self {
        let amount_str = amount.to_string();
        let (ui_amount, ui_amount_string) = Self::calculate_ui_amount(amount, decimals);

        Self {
            amount: amount_str,
            decimals,
            ui_amount,
            ui_amount_string,
        }
    }

    /// Calculate the human-readable amount from raw amount and decimals.
    ///
    /// Note: The `ui_amount` (f64) may lose precision for amounts > 2^53 (~9 quadrillion).
    /// The `ui_amount_string` is always precise as it's computed from integers.
    fn calculate_ui_amount(amount: u64, decimals: u8) -> (Option<f64>, String) {
        if decimals == 0 {
            // Note: May lose precision for very large amounts (> 2^53)
            let ui_amount = amount as f64;
            return (Some(ui_amount), amount.to_string());
        }

        let divisor = 10u64.pow(decimals as u32);
        // Note: May lose precision for very large amounts (> 2^53)
        let ui_amount = amount as f64 / divisor as f64;

        // Format the string with proper decimal places
        let ui_amount_string = if amount == 0 {
            "0".to_string()
        } else {
            // Build the string representation
            let whole = amount / divisor;
            let frac = amount % divisor;

            if frac == 0 {
                whole.to_string()
            } else {
                // Format fractional part with leading zeros and trailing zeros trimmed
                let frac_str = format!("{:0>width$}", frac, width = decimals as usize);
                let frac_str = frac_str.trim_end_matches('0');
                format!("{}.{}", whole, frac_str)
            }
        };

        (Some(ui_amount), ui_amount_string)
    }
}

/// Response for the `getTokenAccountBalance` RPC method.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GetTokenAccountBalanceResponse {
    pub context: RpcResponseContext,
    pub value: TokenAccountBalance,
}

impl GetTokenAccountBalanceResponse {
    pub fn new(slot: u64, amount: u64, decimals: u8) -> Self {
        Self {
            context: RpcResponseContext::new(slot),
            value: TokenAccountBalance::new(amount, decimals),
        }
    }
}

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

    #[test]
    fn test_token_account_balance_no_decimals() {
        let balance = TokenAccountBalance::new(1000, 0);
        assert_eq!(balance.amount, "1000");
        assert_eq!(balance.decimals, 0);
        assert_eq!(balance.ui_amount, Some(1000.0));
        assert_eq!(balance.ui_amount_string, "1000");
    }

    #[test]
    fn test_token_account_balance_with_decimals() {
        // 5 tokens with 9 decimals = 5_000_000_000 raw amount
        let balance = TokenAccountBalance::new(5_000_000_000, 9);
        assert_eq!(balance.amount, "5000000000");
        assert_eq!(balance.decimals, 9);
        assert_eq!(balance.ui_amount, Some(5.0));
        assert_eq!(balance.ui_amount_string, "5");
    }

    #[test]
    fn test_token_account_balance_fractional() {
        // 1.5 tokens with 6 decimals = 1_500_000 raw amount
        let balance = TokenAccountBalance::new(1_500_000, 6);
        assert_eq!(balance.amount, "1500000");
        assert_eq!(balance.decimals, 6);
        assert_eq!(balance.ui_amount, Some(1.5));
        assert_eq!(balance.ui_amount_string, "1.5");
    }

    #[test]
    fn test_token_account_balance_zero() {
        let balance = TokenAccountBalance::new(0, 9);
        assert_eq!(balance.amount, "0");
        assert_eq!(balance.decimals, 9);
        assert_eq!(balance.ui_amount, Some(0.0));
        assert_eq!(balance.ui_amount_string, "0");
    }

    #[test]
    fn test_token_account_balance_small_amount() {
        // 0.000001 tokens with 6 decimals = 1 raw amount
        let balance = TokenAccountBalance::new(1, 6);
        assert_eq!(balance.amount, "1");
        assert_eq!(balance.decimals, 6);
        assert_eq!(balance.ui_amount, Some(0.000001));
        assert_eq!(balance.ui_amount_string, "0.000001");
    }

    #[test]
    fn test_response_serialization() {
        let response = GetTokenAccountBalanceResponse::new(12345, 1_000_000, 6);
        let json = serde_json::to_string(&response).unwrap();
        assert!(json.contains("\"amount\":\"1000000\""));
        assert!(json.contains("\"decimals\":6"));
        assert!(json.contains("\"uiAmountString\":\"1\""));
    }

    #[test]
    fn test_request_validation() {
        use validator::Validate;

        let valid_request = GetTokenAccountBalanceRequest::new(
            "7xB9i2AcjLNJ6M8iZ3LZJvLm7xpSmH7T5uZzR3DeVXWi".to_string(),
        );
        assert!(valid_request.validate().is_ok());

        let empty_request = GetTokenAccountBalanceRequest::new("".to_string());
        assert!(empty_request.validate().is_err());
    }
}
