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

//! Types for the `getInflationReward` RPC method.
//!
//! This method returns epoch rewards for stake and validator accounts.

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

use super::rpc_response_context::RpcResponseContext;

/// Maximum number of addresses allowed in a single getInflationReward request.
pub const MAX_GET_INFLATION_REWARD_ADDRESSES: usize = 1000;

/// Custom validator for inflation reward addresses.
///
/// Validates count (1 to MAX) and that each address is valid base58.
fn validate_inflation_reward_addresses(
    addresses: &[String],
) -> Result<(), validator::ValidationError> {
    if addresses.is_empty() {
        let mut err = validator::ValidationError::new("addresses_empty");
        err.message = Some("At least one address is required".into());
        return Err(err);
    }
    if addresses.len() > MAX_GET_INFLATION_REWARD_ADDRESSES {
        let mut err = validator::ValidationError::new("addresses_exceed_maximum");
        err.message = Some(
            format!(
                "Too many addresses: {} exceeds maximum of {}",
                addresses.len(),
                MAX_GET_INFLATION_REWARD_ADDRESSES
            )
            .into(),
        );
        return Err(err);
    }
    // Validate each address is valid base58
    for (i, addr) in addresses.iter().enumerate() {
        if let Err(e) = crate::validation::validate_pubkey(addr) {
            let mut err = validator::ValidationError::new("invalid_address");
            err.message = Some(format!("Invalid address at index {}: {:?}", i, e).into());
            return Err(err);
        }
    }
    Ok(())
}

/// Request for getInflationReward RPC call.
///
/// # Example
///
/// ```json
/// {
///   "jsonrpc": "2.0",
///   "id": 1,
///   "method": "getInflationReward",
///   "params": [
///     ["<base58-address-1>", "<base58-address-2>"],
///     { "epoch": 100, "minContextSlot": 50000 }
///   ]
/// }
/// ```
#[derive(Debug, Clone, Deserialize, Validate)]
pub struct GetInflationRewardRequest {
    /// Base58-encoded account addresses to query (stake or validator accounts).
    #[validate(custom(function = validate_inflation_reward_addresses))]
    pub addresses: Vec<String>,

    /// Optional configuration.
    #[serde(default)]
    pub config: Option<GetInflationRewardConfig>,
}

impl GetInflationRewardRequest {
    /// Create a new request with the given addresses.
    pub fn new(addresses: Vec<String>) -> Self {
        Self {
            addresses,
            config: None,
        }
    }

    /// Create a new request with addresses and configuration.
    pub fn with_config(addresses: Vec<String>, config: GetInflationRewardConfig) -> Self {
        Self {
            addresses,
            config: Some(config),
        }
    }
}

/// Optional configuration for getInflationReward requests.
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetInflationRewardConfig {
    /// Specific epoch to query rewards for.
    ///
    /// If not specified, defaults to the most recent completed epoch.
    pub epoch: Option<u64>,

    /// Minimum slot the node must have processed before responding.
    ///
    /// If the node hasn't processed this slot yet, returns an error.
    pub min_context_slot: Option<u64>,
}

impl GetInflationRewardConfig {
    /// Create a new config with the given epoch.
    pub fn with_epoch(epoch: u64) -> Self {
        Self {
            epoch: Some(epoch),
            min_context_slot: None,
        }
    }

    /// Create a new config with the given minimum context slot.
    pub fn with_min_context_slot(min_context_slot: u64) -> Self {
        Self {
            epoch: None,
            min_context_slot: Some(min_context_slot),
        }
    }
}

/// Response for getInflationReward RPC call.
///
/// The `value` array matches the order of input addresses.
/// Returns `null` for addresses that are not stake or validator accounts,
/// did not receive rewards in the requested epoch, or were not found in the rewards index.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GetInflationRewardResponse {
    /// Response context including the current slot.
    pub context: RpcResponseContext,

    /// Reward information for each address, in the same order as the request.
    /// Returns `null` for addresses without rewards.
    pub value: Vec<Option<RpcInflationReward>>,
}

impl GetInflationRewardResponse {
    /// Create a new response.
    pub fn new(context: RpcResponseContext, value: Vec<Option<RpcInflationReward>>) -> Self {
        Self { context, value }
    }
}

/// Inflation reward information for a single account.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcInflationReward {
    /// The epoch for which the reward was received.
    pub epoch: u64,

    /// Block height when the reward was credited.
    pub effective_slot: u64,

    /// Reward amount in kelvins.
    pub amount: u64,

    /// Post-reward account balance in kelvins.
    ///
    /// This is the total account balance (including any undelegated funds),
    /// matching Solana's `account.lamports()` behavior.
    pub post_balance: u64,

    /// Validator commission rate when reward was credited (basis points, 0-10000).
    ///
    /// Present for both stake rewards (the commission rate applied) and
    /// validator rewards (the validator's commission rate).
    pub commission: Option<u16>,

    /// For stake rewards: the stake account's reward_receiver field (if configured).
    ///
    /// This indicates where the actual reward was sent. If a stake account has
    /// a `reward_receiver` configured, rewards are transferred to that account
    /// instead of compounding on the stake account.
    ///
    /// `None` for validator rewards or stake accounts without a reward_receiver.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reward_receiver: Option<String>,
}

impl RpcInflationReward {
    /// Create a new inflation reward.
    pub fn new(
        epoch: u64,
        effective_slot: u64,
        amount: u64,
        post_balance: u64,
        commission: Option<u16>,
        reward_receiver: Option<String>,
    ) -> Self {
        Self {
            epoch,
            effective_slot,
            amount,
            post_balance,
            commission,
            reward_receiver,
        }
    }
}

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

    #[test]
    fn test_request_validation_empty() {
        let request = GetInflationRewardRequest::new(vec![]);
        let result = request.validate();
        assert!(result.is_err());
    }

    #[test]
    fn test_request_validation_too_many() {
        let addresses = (0..1001).map(|_| "valid".to_string()).collect();
        let request = GetInflationRewardRequest::new(addresses);
        let result = request.validate();
        assert!(result.is_err());
    }

    #[test]
    fn test_response_serialization() {
        let response = GetInflationRewardResponse::new(
            RpcResponseContext::new(12345),
            vec![
                Some(RpcInflationReward::new(
                    100,
                    50000,
                    1000000,
                    10000000,
                    Some(500),
                    None,
                )),
                None,
                Some(RpcInflationReward::new(
                    100,
                    50000,
                    500000,
                    5000000,
                    Some(0),
                    Some("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM".to_string()),
                )),
            ],
        );

        // Roundtrip test: serialize → deserialize → compare
        let json = serde_json::to_string(&response).unwrap();
        let deserialized: GetInflationRewardResponse = serde_json::from_str(&json).unwrap();
        assert_eq!(response, deserialized);

        // Exact JSON field assertions using parsed JSON
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();

        // Context assertions
        assert_eq!(parsed["context"]["slot"], 12345);

        // First reward (with reward, no reward_receiver)
        let reward0 = &parsed["value"][0];
        assert_eq!(reward0["epoch"], 100);
        assert_eq!(reward0["effectiveSlot"], 50000);
        assert_eq!(reward0["amount"], 1000000);
        assert_eq!(reward0["postBalance"], 10000000);
        assert_eq!(reward0["commission"], 500);
        assert!(reward0.get("rewardReceiver").is_none()); // skip_serializing_if

        // Second entry (null - no rewards)
        assert!(parsed["value"][1].is_null());

        // Third reward (with reward_receiver)
        let reward2 = &parsed["value"][2];
        assert_eq!(reward2["epoch"], 100);
        assert_eq!(reward2["effectiveSlot"], 50000);
        assert_eq!(reward2["amount"], 500000);
        assert_eq!(reward2["postBalance"], 5000000);
        assert_eq!(reward2["commission"], 0);
        assert_eq!(
            reward2["rewardReceiver"],
            "4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM"
        );

        // Array length
        assert_eq!(parsed["value"].as_array().unwrap().len(), 3);
    }

    #[test]
    fn test_reward_receiver_omitted_when_none() {
        let reward = RpcInflationReward::new(100, 50000, 1000000, 10000000, Some(500), None);
        let json = serde_json::to_string(&reward).unwrap();
        // reward_receiver should be omitted (skip_serializing_if)
        assert!(!json.contains("rewardReceiver"));
    }

    #[test]
    fn test_config_deserialization() {
        let json = r#"{"epoch": 100, "minContextSlot": 50000}"#;
        let config: GetInflationRewardConfig = serde_json::from_str(json).unwrap();
        assert_eq!(config.epoch, Some(100));
        assert_eq!(config.min_context_slot, Some(50000));
    }

    #[test]
    fn test_config_deserialization_empty() {
        let json = r#"{}"#;
        let config: GetInflationRewardConfig = serde_json::from_str(json).unwrap();
        assert!(config.epoch.is_none());
        assert!(config.min_context_slot.is_none());
    }
}
