//! Provides an interface for the client to communicate with a Miden node using
//! Remote Procedure Calls (RPC).
//!
//! This module defines the [`NodeRpcClient`] trait which abstracts calls to the RPC protocol used
//! to:
//!
//! - Submit proven transactions.
//! - Retrieve block headers (optionally with MMR proofs).
//! - Sync state updates (including notes, nullifiers, and account updates).
//! - Fetch details for specific notes and accounts.
//!
//! The client implementation adapts to the target environment automatically:
//! - Native targets use `tonic` transport with TLS.
//! - `wasm32` targets use `tonic-web-wasm-client` transport.
//!
//! ## Example
//!
//! ```no_run
//! # use miden_client::rpc::{Endpoint, NodeRpcClient, GrpcClient};
//! # use miden_protocol::block::BlockNumber;
//! # #[tokio::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! // Create a gRPC client instance (assumes default endpoint configuration).
//! let endpoint = Endpoint::new("https".into(), "localhost".into(), Some(57291));
//! let mut rpc_client = GrpcClient::new(&endpoint, 1000);
//!
//! // Fetch the latest block header (by passing None).
//! let (block_header, mmr_proof) = rpc_client.get_block_header_by_number(None, true).await?;
//!
//! println!("Latest block number: {}", block_header.block_num());
//! if let Some(proof) = mmr_proof {
//!     println!("MMR proof received accordingly");
//! }
//!
//! #    Ok(())
//! # }
//! ```
//! The client also makes use of this component in order to communicate with the node.
//!
//! For further details and examples, see the documentation for the individual methods in the
//! [`NodeRpcClient`] trait.

use alloc::boxed::Box;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;

use domain::account::{AccountProof, FetchedAccount};
use domain::note::{FetchedNote, NoteSyncInfo};
use domain::nullifier::NullifierUpdate;
use domain::sync::ChainMmrInfo;
use miden_protocol::Word;
use miden_protocol::account::{Account, AccountCode, AccountHeader, AccountId};
use miden_protocol::address::NetworkId;
use miden_protocol::block::{BlockHeader, BlockNumber, ProvenBlock};
use miden_protocol::crypto::merkle::mmr::MmrProof;
use miden_protocol::crypto::merkle::smt::SmtProof;
use miden_protocol::note::{NoteId, NoteScript, NoteTag, Nullifier};
use miden_protocol::transaction::{ProvenTransaction, TransactionInputs};

/// Contains domain types related to RPC requests and responses, as well as utility functions
/// for dealing with them.
pub mod domain;

mod errors;
pub use errors::*;

mod endpoint;
pub(crate) use domain::limits::RPC_LIMITS_STORE_SETTING;
pub use domain::limits::RpcLimits;
pub use domain::status::RpcStatusInfo;
pub use endpoint::Endpoint;

#[cfg(not(feature = "testing"))]
mod generated;
#[cfg(feature = "testing")]
pub mod generated;

#[cfg(feature = "tonic")]
mod tonic_client;
#[cfg(feature = "tonic")]
pub use tonic_client::GrpcClient;

use crate::rpc::domain::account::AccountStorageRequirements;
use crate::rpc::domain::account_vault::AccountVaultInfo;
use crate::rpc::domain::storage_map::StorageMapInfo;
use crate::rpc::domain::transaction::TransactionsInfo;
use crate::store::InputNoteRecord;
use crate::store::input_note_states::UnverifiedNoteState;

/// Represents the state that we want to retrieve from the network
pub enum AccountStateAt {
    /// Gets the latest state, for the current chain tip
    ChainTip,
    /// Gets the state at a specific block number
    Block(BlockNumber),
}

// NODE RPC CLIENT TRAIT
// ================================================================================================

/// Defines the interface for communicating with the Miden node.
///
/// The implementers are responsible for connecting to the Miden node, handling endpoint
/// requests/responses, and translating responses into domain objects relevant for each of the
/// endpoints.
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
pub trait NodeRpcClient: Send + Sync {
    /// Sets the genesis commitment for the client and reconnects to the node providing the
    /// genesis commitment in the request headers. If the genesis commitment is already set,
    /// this method does nothing.
    async fn set_genesis_commitment(&self, commitment: Word) -> Result<(), RpcError>;

    /// Returns the genesis commitment if it has been set, without fetching from the node.
    fn has_genesis_commitment(&self) -> Option<Word>;

    /// Given a Proven Transaction, send it to the node for it to be included in a future block
    /// using the `/SubmitProvenTransaction` RPC endpoint.
    async fn submit_proven_transaction(
        &self,
        proven_transaction: ProvenTransaction,
        transaction_inputs: TransactionInputs,
    ) -> Result<BlockNumber, RpcError>;

    /// Given a block number, fetches the block header corresponding to that height from the node
    /// using the `/GetBlockHeaderByNumber` endpoint.
    /// If `include_mmr_proof` is set to true and the function returns an `Ok`, the second value
    /// of the return tuple should always be Some(MmrProof).
    ///
    /// When `None` is provided, returns info regarding the latest block.
    async fn get_block_header_by_number(
        &self,
        block_num: Option<BlockNumber>,
        include_mmr_proof: bool,
    ) -> Result<(BlockHeader, Option<MmrProof>), RpcError>;

    /// Given a block number, fetches the block corresponding to that height from the node using
    /// the `/GetBlockByNumber` RPC endpoint.
    async fn get_block_by_number(&self, block_num: BlockNumber) -> Result<ProvenBlock, RpcError>;

    /// Fetches note-related data for a list of [`NoteId`] using the `/GetNotesById`
    /// RPC endpoint.
    ///
    /// For [`miden_protocol::note::NoteType::Private`] notes, the response includes only the
    /// [`miden_protocol::note::NoteMetadata`].
    ///
    /// For [`miden_protocol::note::NoteType::Public`] notes, the response includes all note details
    /// (recipient, assets, script, etc.).
    ///
    /// In both cases, a [`miden_protocol::note::NoteInclusionProof`] is returned so the caller can
    /// verify that each note is part of the block's note tree.
    async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError>;

    /// Fetches the MMR delta for a given block range using the `/SyncChainMmr` RPC endpoint.
    ///
    /// - `block_from` is the last block number already present in the caller's MMR.
    /// - `block_to` is the optional upper bound of the range. If `None`, syncs up to the chain tip.
    async fn sync_chain_mmr(
        &self,
        block_from: BlockNumber,
        block_to: Option<BlockNumber>,
    ) -> Result<ChainMmrInfo, RpcError>;

    /// Fetches the current state of an account from the node using the `/GetAccountDetails` RPC
    /// endpoint.
    ///
    /// - `account_id` is the ID of the wanted account.
    async fn get_account_details(&self, account_id: AccountId) -> Result<FetchedAccount, RpcError>;

    /// Fetches the notes related to the specified tags using the `/SyncNotes` RPC endpoint.
    ///
    /// - `block_num` is the last block number known by the client.
    /// - `note_tags` is a list of tags used to filter the notes the client is interested in.
    async fn sync_notes(
        &self,
        block_num: BlockNumber,
        block_to: Option<BlockNumber>,
        note_tags: &BTreeSet<NoteTag>,
    ) -> Result<NoteSyncInfo, RpcError>;

    /// Fetches the nullifiers corresponding to a list of prefixes using the
    /// `/SyncNullifiers` RPC endpoint.
    ///
    /// - `prefix` is a list of nullifiers prefixes to search for.
    /// - `block_num` is the block number to start the search from. Nullifiers created in this block
    ///   or the following blocks will be included.
    /// - `block_to` is the optional block number to stop the search at. If not provided, syncs up
    ///   to the network chain tip.
    async fn sync_nullifiers(
        &self,
        prefix: &[u16],
        block_num: BlockNumber,
        block_to: Option<BlockNumber>,
    ) -> Result<Vec<NullifierUpdate>, RpcError>;

    /// Fetches the nullifier proofs corresponding to a list of nullifiers using the
    /// `/CheckNullifiers` RPC endpoint.
    async fn check_nullifiers(&self, nullifiers: &[Nullifier]) -> Result<Vec<SmtProof>, RpcError>;

    /// Fetches the account proof and optionally its details from the node, using the
    /// `GetAccountProof` endpoint.
    ///
    /// The `account_state` parameter specifies the block number from which to retrieve
    /// the account proof from (the state of the account at that block).
    ///
    /// The `storage_requirements` parameter specifies which storage slots and map keys
    /// should be included in the response for public accounts.
    ///
    /// The `known_account_code` parameter is the known code commitment
    /// to prevent unnecessary data fetching. Returns the block number and the account proof. If
    /// the account is not found in the node, the method will return an error.
    async fn get_account_proof(
        &self,
        account_id: AccountId,
        storage_requirements: AccountStorageRequirements,
        account_state: AccountStateAt,
        known_account_code: Option<AccountCode>,
    ) -> Result<(BlockNumber, AccountProof), RpcError>;

    /// Fetches the commit height where the nullifier was consumed. If the nullifier isn't found,
    /// then `None` is returned.
    /// The `block_num` parameter is the block number to start the search from.
    ///
    /// The default implementation of this method uses
    /// [`NodeRpcClient::sync_nullifiers`].
    async fn get_nullifier_commit_heights(
        &self,
        requested_nullifiers: BTreeSet<Nullifier>,
        block_from: BlockNumber,
    ) -> Result<BTreeMap<Nullifier, Option<BlockNumber>>, RpcError> {
        let prefixes: Vec<u16> =
            requested_nullifiers.iter().map(crate::note::Nullifier::prefix).collect();
        let retrieved_nullifiers = self.sync_nullifiers(&prefixes, block_from, None).await?;

        let mut nullifiers_height = BTreeMap::new();
        for nullifier in requested_nullifiers {
            if let Some(update) =
                retrieved_nullifiers.iter().find(|update| update.nullifier == nullifier)
            {
                nullifiers_height.insert(nullifier, Some(update.block_num));
            } else {
                nullifiers_height.insert(nullifier, None);
            }
        }

        Ok(nullifiers_height)
    }

    /// Fetches public note-related data for a list of [`NoteId`] and builds [`InputNoteRecord`]s
    /// with it. If a note is not found or it's private, it is ignored and will not be included
    /// in the returned list.
    ///
    /// The default implementation of this method uses [`NodeRpcClient::get_notes_by_id`].
    async fn get_public_note_records(
        &self,
        note_ids: &[NoteId],
        current_timestamp: Option<u64>,
    ) -> Result<Vec<InputNoteRecord>, RpcError> {
        if note_ids.is_empty() {
            return Ok(vec![]);
        }

        let mut public_notes = Vec::with_capacity(note_ids.len());
        let note_details = self.get_notes_by_id(note_ids).await?;

        for detail in note_details {
            if let FetchedNote::Public(note, inclusion_proof) = detail {
                let state = UnverifiedNoteState {
                    metadata: note.metadata().clone(),
                    inclusion_proof,
                }
                .into();
                let note = InputNoteRecord::new(note.into(), current_timestamp, state);

                public_notes.push(note);
            }
        }

        Ok(public_notes)
    }

    /// Fetches the public accounts that have been updated since the last known state of the
    /// accounts.
    ///
    /// The `local_accounts` parameter is a list of account headers that the client has
    /// stored locally and that it wants to check for updates. If an account is private or didn't
    /// change, it is ignored and will not be included in the returned list.
    /// The default implementation of this method uses [`NodeRpcClient::get_account_details`].
    async fn get_updated_public_accounts(
        &self,
        local_accounts: &[&AccountHeader],
    ) -> Result<Vec<Account>, RpcError> {
        let mut public_accounts = vec![];

        for local_account in local_accounts {
            let response = self.get_account_details(local_account.id()).await?;

            if let FetchedAccount::Public(account, _) = response {
                let account = *account;
                // We should only return an account if it's newer, otherwise we ignore it
                if account.nonce().as_int() > local_account.nonce().as_int() {
                    public_accounts.push(account);
                }
            }
        }

        Ok(public_accounts)
    }

    /// Given a block number, fetches the block header corresponding to that height from the node
    /// along with the MMR proof.
    ///
    /// The default implementation of this method uses
    /// [`NodeRpcClient::get_block_header_by_number`].
    async fn get_block_header_with_proof(
        &self,
        block_num: BlockNumber,
    ) -> Result<(BlockHeader, MmrProof), RpcError> {
        let (header, proof) = self.get_block_header_by_number(Some(block_num), true).await?;
        Ok((header, proof.ok_or(RpcError::ExpectedDataMissing(String::from("MmrProof")))?))
    }

    /// Fetches the note with the specified ID.
    ///
    /// The default implementation of this method uses [`NodeRpcClient::get_notes_by_id`].
    ///
    /// Errors:
    /// - [`RpcError::NoteNotFound`] if the note with the specified ID is not found.
    async fn get_note_by_id(&self, note_id: NoteId) -> Result<FetchedNote, RpcError> {
        let notes = self.get_notes_by_id(&[note_id]).await?;
        notes.into_iter().next().ok_or(RpcError::NoteNotFound(note_id))
    }

    /// Fetches the note script with the specified root.
    ///
    /// Errors:
    /// - [`RpcError::ExpectedDataMissing`] if the note with the specified root is not found.
    async fn get_note_script_by_root(&self, root: Word) -> Result<NoteScript, RpcError>;

    /// Fetches storage map updates for specified account and storage slots within a block range,
    /// using the `/SyncStorageMaps` RPC endpoint.
    ///
    /// - `block_from`: The starting block number for the range.
    /// - `block_to`: The ending block number for the range.
    /// - `account_id`: The account ID for which to fetch storage map updates.
    async fn sync_storage_maps(
        &self,
        block_from: BlockNumber,
        block_to: Option<BlockNumber>,
        account_id: AccountId,
    ) -> Result<StorageMapInfo, RpcError>;

    /// Fetches account vault updates for specified account within a block range,
    /// using the `/SyncAccountVault` RPC endpoint.
    ///
    /// - `block_from`: The starting block number for the range.
    /// - `block_to`: The ending block number for the range.
    /// - `account_id`: The account ID for which to fetch storage map updates.
    async fn sync_account_vault(
        &self,
        block_from: BlockNumber,
        block_to: Option<BlockNumber>,
        account_id: AccountId,
    ) -> Result<AccountVaultInfo, RpcError>;

    /// Fetches transactions records for specific accounts within a block range.
    /// Using the `/SyncTransactions` RPC endpoint.
    ///
    /// - `block_from`: The starting block number for the range.
    /// - `block_to`: The ending block number for the range.
    /// - `account_ids`: The account IDs for which to fetch storage map updates.
    async fn sync_transactions(
        &self,
        block_from: BlockNumber,
        block_to: Option<BlockNumber>,
        account_ids: Vec<AccountId>,
    ) -> Result<TransactionsInfo, RpcError>;

    /// Fetches the network ID of the node.
    /// Errors:
    /// - [`RpcError::ExpectedDataMissing`] if the note with the specified root is not found.
    async fn get_network_id(&self) -> Result<NetworkId, RpcError>;

    /// Fetches the RPC limits configured on the node.
    ///
    /// Implementations may cache the result internally to avoid repeated network calls.
    async fn get_rpc_limits(&self) -> Result<RpcLimits, RpcError>;

    /// Returns the RPC limits if they have been set, without fetching from the node.
    fn has_rpc_limits(&self) -> Option<RpcLimits>;

    /// Sets the RPC limits internally to be used by the client.
    async fn set_rpc_limits(&self, limits: RpcLimits);

    /// Fetches the RPC status without requiring Accept header validation.
    ///
    /// This is useful for diagnostics when version negotiation fails, as it allows
    /// retrieving node information even when there's a version mismatch.
    async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError>;
}

// RPC API ENDPOINT
// ================================================================================================
//
/// RPC methods for the Miden protocol.
#[derive(Debug, Clone, Copy)]
pub enum RpcEndpoint {
    Status,
    CheckNullifiers,
    SyncNullifiers,
    GetAccount,
    GetBlockByNumber,
    GetBlockHeaderByNumber,
    GetNotesById,
    SyncChainMmr,
    SubmitProvenTx,
    SyncNotes,
    GetNoteScriptByRoot,
    SyncStorageMaps,
    SyncAccountVault,
    SyncTransactions,
    GetLimits,
}

impl RpcEndpoint {
    /// Returns the endpoint name as used in the RPC service definition.
    pub fn proto_name(&self) -> &'static str {
        match self {
            RpcEndpoint::Status => "Status",
            RpcEndpoint::CheckNullifiers => "CheckNullifiers",
            RpcEndpoint::SyncNullifiers => "SyncNullifiers",
            RpcEndpoint::GetAccount => "GetAccount",
            RpcEndpoint::GetBlockByNumber => "GetBlockByNumber",
            RpcEndpoint::GetBlockHeaderByNumber => "GetBlockHeaderByNumber",
            RpcEndpoint::GetNotesById => "GetNotesById",
            RpcEndpoint::SyncChainMmr => "SyncChainMmr",
            RpcEndpoint::SubmitProvenTx => "SubmitProvenTransaction",
            RpcEndpoint::SyncNotes => "SyncNotes",
            RpcEndpoint::GetNoteScriptByRoot => "GetNoteScriptByRoot",
            RpcEndpoint::SyncStorageMaps => "SyncStorageMaps",
            RpcEndpoint::SyncAccountVault => "SyncAccountVault",
            RpcEndpoint::SyncTransactions => "SyncTransactions",
            RpcEndpoint::GetLimits => "GetLimits",
        }
    }
}

impl fmt::Display for RpcEndpoint {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            RpcEndpoint::Status => write!(f, "status"),
            RpcEndpoint::CheckNullifiers => write!(f, "check_nullifiers"),
            RpcEndpoint::SyncNullifiers => {
                write!(f, "sync_nullifiers")
            },
            RpcEndpoint::GetAccount => write!(f, "get_account_proof"),
            RpcEndpoint::GetBlockByNumber => write!(f, "get_block_by_number"),
            RpcEndpoint::GetBlockHeaderByNumber => {
                write!(f, "get_block_header_by_number")
            },
            RpcEndpoint::GetNotesById => write!(f, "get_notes_by_id"),
            RpcEndpoint::SyncChainMmr => write!(f, "sync_chain_mmr"),
            RpcEndpoint::SubmitProvenTx => write!(f, "submit_proven_transaction"),
            RpcEndpoint::SyncNotes => write!(f, "sync_notes"),
            RpcEndpoint::GetNoteScriptByRoot => write!(f, "get_note_script_by_root"),
            RpcEndpoint::SyncStorageMaps => write!(f, "sync_storage_maps"),
            RpcEndpoint::SyncAccountVault => write!(f, "sync_account_vault"),
            RpcEndpoint::SyncTransactions => write!(f, "sync_transactions"),
            RpcEndpoint::GetLimits => write!(f, "get_limits"),
        }
    }
}
