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

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

use super::rpc_response_context::RpcResponseContext;

/// Filter for getAccountsByOwner to specify what kind of accounts to return.
///
/// This enum defines the different types of account queries supported:
/// - `ProgramAccounts`: Query accounts by their on-chain owner (program ID)
/// - `TokenAccounts`: Query token accounts by the wallet that holds the tokens
/// - `StakeAccounts`: Query stake accounts by authority (staker/withdrawer/validator)
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum AccountFilter {
    /// Return accounts owned by a program.
    ///
    /// The `owner` parameter in the request should be a program ID.
    /// Returns all accounts where `account.owner == program_id`.
    #[default]
    #[serde(rename = "programAccounts")]
    ProgramAccounts,

    /// Return token accounts belonging to a wallet.
    /// Use this to get "all tokens/NFTs for a wallet".
    /// NFTs are automatically included since they are also token accounts.
    ///
    /// The `owner` parameter in the request should be a wallet address.
    /// Returns all token accounts where the token struct's owner field matches.
    #[serde(rename = "tokenAccounts")]
    TokenAccounts {
        /// Optional: filter by specific mint address (for performance).
        /// Returns the wallet's token account(s) for this specific mint only.
        ///
        /// While technically possible to have multiple token accounts per mint,
        /// in practice Associated Token Accounts (ATAs) are used exclusively,
        /// meaning each wallet has at most ONE account per mint.
        #[serde(skip_serializing_if = "Option::is_none")]
        mint: Option<String>,
    },

    /// Return stake accounts for a user.
    ///
    /// The `owner` parameter can be used in different ways based on the `authority` field:
    /// - `staker`: Returns stake accounts where the user is the staker authority
    /// - `withdrawer`: Returns stake accounts where the user is the withdrawer authority
    /// - `validator`: Returns stake accounts delegated to this validator
    ///
    /// Default authority is `staker`.
    #[serde(rename = "stakeAccounts")]
    StakeAccounts {
        /// Which authority to query by: "staker" (default), "withdrawer", or "validator".
        /// - staker: The authority that can delegate/deactivate the stake
        /// - withdrawer: The authority that can withdraw funds
        /// - validator: The validator account the stake is delegated to
        #[serde(skip_serializing_if = "Option::is_none")]
        authority: Option<StakeAuthority>,

        /// Optional: filter by specific validator address.
        /// Only return stake accounts delegated to this specific validator.
        /// Only applicable when authority is "staker" or "withdrawer".
        #[serde(skip_serializing_if = "Option::is_none")]
        validator: Option<String>,
    },
    // Future extensions:
    // NonceAccounts,
}

/// The type of stake authority to query by.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
pub enum StakeAuthority {
    /// Query by staker authority (can delegate/deactivate).
    #[default]
    Staker,
    /// Query by withdrawer authority (can withdraw funds).
    Withdrawer,
    /// Query by validator.
    Validator,
}

impl AccountFilter {
    /// Validate stake accounts filter configuration.
    ///
    /// Returns an error if the validator filter is provided when querying by validator authority,
    /// as this combination is semantically invalid.
    ///
    /// # Default Authority
    ///
    /// When `authority` is `None`, it defaults to `Staker` (see `StakeAuthority::default()`).
    /// This validation uses the effective authority (applying the default) to ensure
    /// consistent behavior regardless of whether the default is explicitly specified.
    pub fn validate(&self) -> Result<(), String> {
        if let AccountFilter::StakeAccounts {
            authority,
            validator,
        } = self
        {
            // Use effective authority (apply default of Staker when None)
            let effective_authority = authority.as_ref().unwrap_or(&StakeAuthority::Staker);

            // The validator filter is only applicable when querying by staker or withdrawer.
            // When querying by validator (authority=Validator), the owner param IS the validator,
            // so providing an additional validator filter makes no sense.
            if matches!(effective_authority, StakeAuthority::Validator) && validator.is_some() {
                return Err(
                    "The 'validator' filter is not applicable when querying by validator authority. \
                     When authority is 'validator', the owner parameter is the validator to query."
                        .to_string(),
                );
            }
        }
        Ok(())
    }
}

/// Configuration for getAccountsByOwner request.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct GetAccountsByOwnerConfig {
    /// Maximum number of accounts to return (used with indexer).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,

    /// Cursor for pagination: accounts after this pubkey (base58-encoded).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub after: Option<String>,
}

impl GetAccountsByOwnerConfig {
    pub fn new() -> Self {
        Self {
            limit: None,
            after: None,
        }
    }

    /// Create config with pagination parameters.
    pub fn with_pagination(limit: Option<usize>, after: Option<String>) -> Self {
        Self { limit, after }
    }
}

/// Request parameters for the `getAccountsByOwner` RPC method.
///
/// This method returns accounts based on the filter type:
/// - For `ProgramAccounts`: Returns all accounts owned by a given program
/// - For `TokenAccounts`: Returns all token accounts belonging to a wallet
/// - For `StakeAccounts`: Returns stake accounts by authority
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
#[serde(rename_all = "camelCase")]
pub struct GetAccountsByOwnerRequest {
    #[serde(default)]
    #[validate(custom(function = crate::validation::validate_protocol_version))]
    pub version: u16,

    /// The owner pubkey to query accounts for.
    /// - For ProgramAccounts: this is the program ID
    /// - For TokenAccounts: this is the wallet address
    /// - For StakeAccounts: this is the authority address (staker/withdrawer/validator)
    #[validate(length(min = 1))]
    pub owner: String,

    /// Filter to specify what kind of accounts to return.
    #[serde(default)]
    pub filter: AccountFilter,

    /// Optional configuration for pagination.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub config: Option<GetAccountsByOwnerConfig>,
}

impl GetAccountsByOwnerRequest {
    /// Create a new request for program accounts (default filter).
    pub fn new(owner: String) -> Self {
        Self {
            version: 0,
            owner,
            filter: AccountFilter::default(),
            config: Some(GetAccountsByOwnerConfig::default()),
        }
    }

    /// Create a new request with a specific filter.
    pub fn with_filter(owner: String, filter: AccountFilter) -> Self {
        Self {
            version: 0,
            owner,
            filter,
            config: Some(GetAccountsByOwnerConfig::default()),
        }
    }

    /// Create a new request with filter and config.
    pub fn with_config(
        owner: String,
        filter: AccountFilter,
        config: GetAccountsByOwnerConfig,
    ) -> Self {
        Self {
            version: 0,
            owner,
            filter,
            config: Some(config),
        }
    }
}

/// A single account returned by getAccountsByOwner.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OwnerAccount {
    /// The account's public key (base58-encoded).
    pub pubkey: String,
    /// The account information.
    pub account: AccountInfoResponseValue,
}

/// Pagination information for responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationInfo {
    /// Whether there are more results after this page.
    pub has_more: bool,
    /// Cursor for the next page (base58-encoded pubkey), if has_more is true.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub next_cursor: Option<String>,
}

/// Response from the `getAccountsByOwner` RPC method.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetAccountsByOwnerResponse {
    #[serde(default)]
    pub version: u16,
    /// Response context with slot information.
    pub context: RpcResponseContext,
    /// Array of accounts matching the filter criteria.
    pub value: Vec<OwnerAccount>,
    /// Pagination info (only present when using indexed queries with pagination).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pagination: Option<PaginationInfo>,
}

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

    #[test]
    fn test_get_accounts_by_owner_request_serialization() {
        let request =
            GetAccountsByOwnerRequest::new("11111111111111111111111111111111".to_string());
        let json = serde_json::to_string(&request).unwrap();
        assert!(json.contains("11111111111111111111111111111111"));
        assert!(json.contains("programAccounts"));
    }

    #[test]
    fn test_get_accounts_by_owner_config() {
        let config = GetAccountsByOwnerConfig::new();
        assert_eq!(config.limit, None);
        assert_eq!(config.after, None);
    }

    #[test]
    fn test_account_filter_default() {
        let filter = AccountFilter::default();
        assert_eq!(filter, AccountFilter::ProgramAccounts);
    }

    #[test]
    fn test_program_accounts_filter_serialization() {
        let filter = AccountFilter::ProgramAccounts;
        let json = serde_json::to_string(&filter).unwrap();
        assert_eq!(json, r#"{"type":"programAccounts"}"#);
    }

    #[test]
    fn test_token_accounts_filter_serialization() {
        let filter = AccountFilter::TokenAccounts { mint: None };
        let json = serde_json::to_string(&filter).unwrap();
        assert_eq!(json, r#"{"type":"tokenAccounts"}"#);

        let filter_with_mint = AccountFilter::TokenAccounts {
            mint: Some("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string()),
        };
        let json_with_mint = serde_json::to_string(&filter_with_mint).unwrap();
        assert!(json_with_mint.contains("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"));
    }

    #[test]
    fn test_stake_accounts_filter_serialization() {
        // Default (staker authority)
        let filter = AccountFilter::StakeAccounts {
            authority: None,
            validator: None,
        };
        let json = serde_json::to_string(&filter).unwrap();
        assert_eq!(json, r#"{"type":"stakeAccounts"}"#);

        // With explicit staker authority
        let filter = AccountFilter::StakeAccounts {
            authority: Some(StakeAuthority::Staker),
            validator: None,
        };
        let json = serde_json::to_string(&filter).unwrap();
        assert!(json.contains("staker"));

        // With withdrawer authority
        let filter = AccountFilter::StakeAccounts {
            authority: Some(StakeAuthority::Withdrawer),
            validator: None,
        };
        let json = serde_json::to_string(&filter).unwrap();
        assert!(json.contains("withdrawer"));

        // With validator authority
        let filter = AccountFilter::StakeAccounts {
            authority: Some(StakeAuthority::Validator),
            validator: None,
        };
        let json = serde_json::to_string(&filter).unwrap();
        assert!(json.contains("validator"));

        // With staker + validator filter
        let filter = AccountFilter::StakeAccounts {
            authority: Some(StakeAuthority::Staker),
            validator: Some("Validator111111111111111111111111111111".to_string()),
        };
        let json = serde_json::to_string(&filter).unwrap();
        assert!(json.contains("staker"));
        assert!(json.contains("Validator111111111111111111111111111111"));
    }

    #[test]
    fn test_stake_accounts_filter_deserialization() {
        // Default
        let json = r#"{"type":"stakeAccounts"}"#;
        let filter: AccountFilter = serde_json::from_str(json).unwrap();
        assert!(matches!(
            filter,
            AccountFilter::StakeAccounts {
                authority: None,
                validator: None
            }
        ));

        // With staker authority
        let json = r#"{"type":"stakeAccounts","authority":"staker"}"#;
        let filter: AccountFilter = serde_json::from_str(json).unwrap();
        assert!(matches!(
            filter,
            AccountFilter::StakeAccounts {
                authority: Some(StakeAuthority::Staker),
                validator: None
            }
        ));

        // With validator filter
        let json = r#"{"type":"stakeAccounts","validator":"ABC123"}"#;
        let filter: AccountFilter = serde_json::from_str(json).unwrap();
        assert!(matches!(
            filter,
            AccountFilter::StakeAccounts {
                authority: None,
                validator: Some(v)
            } if v == "ABC123"
        ));
    }

    #[test]
    fn test_account_filter_deserialization() {
        let json = r#"{"type":"programAccounts"}"#;
        let filter: AccountFilter = serde_json::from_str(json).unwrap();
        assert_eq!(filter, AccountFilter::ProgramAccounts);

        let json = r#"{"type":"tokenAccounts"}"#;
        let filter: AccountFilter = serde_json::from_str(json).unwrap();
        assert!(matches!(
            filter,
            AccountFilter::TokenAccounts { mint: None }
        ));

        let json = r#"{"type":"tokenAccounts","mint":"ABC123"}"#;
        let filter: AccountFilter = serde_json::from_str(json).unwrap();
        assert!(matches!(
            filter,
            AccountFilter::TokenAccounts {
                mint: Some(m)
            } if m == "ABC123"
        ));
    }

    #[test]
    fn test_request_with_filter() {
        let request = GetAccountsByOwnerRequest::with_filter(
            "MyWallet111111111111111111111111111111111111".to_string(),
            AccountFilter::TokenAccounts {
                mint: Some("USDC1111111111111111111111111111111111111111".to_string()),
            },
        );
        let json = serde_json::to_string(&request).unwrap();
        assert!(json.contains("tokenAccounts"));
        assert!(json.contains("USDC1111111111111111111111111111111111111111"));
    }

    #[test]
    fn test_request_with_stake_filter() {
        let request = GetAccountsByOwnerRequest::with_filter(
            "MyWallet111111111111111111111111111111111111".to_string(),
            AccountFilter::StakeAccounts {
                authority: Some(StakeAuthority::Staker),
                validator: Some("Validator111111111111111111111111111111".to_string()),
            },
        );
        let json = serde_json::to_string(&request).unwrap();
        assert!(json.contains("stakeAccounts"));
        assert!(json.contains("staker"));
        assert!(json.contains("Validator111111111111111111111111111111"));
    }

    #[test]
    fn test_config_with_pagination() {
        let config = GetAccountsByOwnerConfig::with_pagination(
            Some(100),
            Some("CursorPubkey111111111111111111111111111111".to_string()),
        );
        assert_eq!(config.limit, Some(100));
        assert_eq!(
            config.after,
            Some("CursorPubkey111111111111111111111111111111".to_string())
        );
    }

    // =========================================================================
    // AccountFilter::validate() tests
    // =========================================================================

    #[test]
    fn test_account_filter_validate_program_accounts() {
        let filter = AccountFilter::ProgramAccounts;
        assert!(filter.validate().is_ok());
    }

    #[test]
    fn test_account_filter_validate_token_accounts() {
        let filter = AccountFilter::TokenAccounts { mint: None };
        assert!(filter.validate().is_ok());

        let filter_with_mint = AccountFilter::TokenAccounts {
            mint: Some("ABC".to_string()),
        };
        assert!(filter_with_mint.validate().is_ok());
    }

    #[test]
    fn test_account_filter_validate_stake_accounts_staker_with_validator_filter() {
        // Staker + validator filter is valid (filter by delegated validator)
        let filter = AccountFilter::StakeAccounts {
            authority: Some(StakeAuthority::Staker),
            validator: Some("Validator111111111111111111111111111111".to_string()),
        };
        assert!(filter.validate().is_ok());
    }

    #[test]
    fn test_account_filter_validate_stake_accounts_withdrawer_with_validator_filter() {
        // Withdrawer + validator filter is valid
        let filter = AccountFilter::StakeAccounts {
            authority: Some(StakeAuthority::Withdrawer),
            validator: Some("Validator111111111111111111111111111111".to_string()),
        };
        assert!(filter.validate().is_ok());
    }

    #[test]
    fn test_account_filter_validate_stake_accounts_validator_without_validator_filter() {
        // Validator authority without validator filter is valid
        let filter = AccountFilter::StakeAccounts {
            authority: Some(StakeAuthority::Validator),
            validator: None,
        };
        assert!(filter.validate().is_ok());
    }

    #[test]
    fn test_account_filter_validate_stake_accounts_validator_with_validator_filter_invalid() {
        // Validator authority + validator filter is INVALID (semantically nonsensical)
        let filter = AccountFilter::StakeAccounts {
            authority: Some(StakeAuthority::Validator),
            validator: Some("SomeValidator111111111111111111111111111111111".to_string()),
        };
        let result = filter.validate();
        assert!(result.is_err());
        assert!(result
            .unwrap_err()
            .contains("not applicable when querying by validator authority"));
    }
}
