#![allow(clippy::type_complexity)]
#![allow(missing_docs)]
#![deny(unsafe_code)]

//! # Neo Clients
//!
//! Client interfaces for interacting with Neo N3 blockchain nodes.
//!
//! ## Overview
//!
//! The neo_clients module provides a comprehensive set of client interfaces for connecting to
//! and interacting with Neo N3 blockchain nodes. It includes:
//!
//! - RPC clients for making JSON-RPC calls to Neo nodes
//! - Multiple transport providers (HTTP, WebSocket, IPC)
//! - Subscription support for real-time blockchain events
//! - Mock clients for testing
//! - Extension traits for domain-specific functionality
//! - Error handling for network and protocol issues
//!
//! The module is designed to be flexible, allowing developers to choose the appropriate
//! client implementation and transport mechanism for their specific use case.
//!
//! ## Examples
//!
//! ### Connecting to a Neo N3 node using HTTP
//!
//! ```rust,no_run
//! use neo3::neo_clients::{HttpProvider, RpcClient, APITrait};
//!
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//!     // Create an HTTP provider connected to a Neo N3 TestNet node
//!     let provider = HttpProvider::new("https://testnet1.neo.org:443")?;
//!     
//!     // Create an RPC client with the provider
//!     let client = RpcClient::new(provider);
//!     
//!     // Get the current block count
//!     let block_count = client.get_block_count().await?;
//!     println!("Current block count: {}", block_count);
//!     
//!     // Get information about the blockchain
//!     let version = client.get_version().await?;
//!     println!("Node version: {}", version.user_agent);
//!     
//!     Ok(())
//! }
//! ```
//!
//! ### Using WebSocket for real-time updates
//!
//! ```ignore
//! # #[cfg(feature = "ws")]
//! use neo3::neo_clients::{Ws, RpcClient, APITrait};
//!
//! # #[cfg(feature = "ws")]
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//!     // Connect to a Neo N3 node using WebSocket
//!     let ws = Ws::connect("wss://testnet1.neo.org:443/ws").await?;
//!     let client = RpcClient::new(ws);
//!     
//!     // Get basic blockchain information over WebSocket
//!     let block_count = client.get_block_count().await?;
//!     println!("Current block count: {}", block_count);
//!     
//!     let version = client.get_version().await?;
//!     println!("Node version: {}", version.user_agent);
//!     
//!     Ok(())
//! }
//! ```

use lazy_static::lazy_static;

use crate::config::NeoConstants;

/// Common interface for JSON-RPC transport errors so callers can inspect structured responses.
pub trait RpcError: std::error::Error + Send + Sync {
	/// Returns the underlying JSON-RPC error if available.
	fn as_error_response(&self) -> Option<&rpc::JsonRpcError> {
		None
	}

	/// Returns the underlying serde_json error if available.
	fn as_serde_error(&self) -> Option<&serde_json::Error> {
		None
	}
}
pub use api_trait::*;
pub use cache::{Cache, CacheConfig, CacheStats, RpcCache};
pub use circuit_breaker::{
	CircuitBreaker, CircuitBreakerConfig, CircuitBreakerStats, CircuitState,
};
pub use connection_pool::{ConnectionPool, PoolConfig, PoolStats};
pub use errors::ProviderError;
pub use ext::*;
#[cfg(any(test, feature = "mock"))]
pub use mock_client::MockClient;
pub use production_client::{ProductionClientConfig, ProductionClientStats, ProductionRpcClient};
pub use rate_limiter::{RateLimitPermit, RateLimiter, RateLimiterBuilder, RateLimiterPresets};
pub use rpc::*;
#[deprecated(
	note = "Use `HttpProvider::new(...)`/`RpcClient::new(...)` or the high-level `sdk::Neo` builder instead.",
	since = "0.5.4"
)]
pub use test_provider::{MAINNET, TESTNET};
pub use utils::*;

mod api_trait;
mod cache;
mod circuit_breaker;
mod connection_pool;
/// Errors
mod errors;
mod ext;
mod mock_blocks;
#[cfg(any(test, feature = "mock"))]
mod mock_client;
mod production_client;
mod rate_limiter;
mod rpc;
mod rx;
/// Crate utilities and type aliases
mod utils;

lazy_static! {
	pub static ref HTTP_PROVIDER: RpcClient<Http> = {
		let url_str =
			std::env::var("ENDPOINT").unwrap_or_else(|_| NeoConstants::SEED_1.to_string());
		let url = url::Url::parse(&url_str).unwrap_or_else(|e| {
			tracing::warn!(
				error = %e,
				endpoint = %url_str,
				"Failed to parse ENDPOINT; falling back to default seed URL"
			);
			url::Url::parse(NeoConstants::SEED_1).expect("NeoConstants::SEED_1 must be a valid URL")
		});
		let http_provider = Http::new(url).unwrap_or_else(|e| {
			tracing::warn!(
				error = %e,
				"Failed to create HTTP provider; falling back to default seed URL"
			);
			let url = url::Url::parse(NeoConstants::SEED_1)
				.expect("NeoConstants::SEED_1 must be a valid URL");
			Http::new(url).expect("Should be able to create HTTP provider for default seed URL")
		});
		RpcClient::new(http_provider)
	};
}

#[allow(missing_docs)]
/// Deprecated pre-instantiated providers for quick-start/testing.
///
/// Prefer explicit configuration via `HttpProvider::new(...)`, `RpcClient::new(...)`,
/// or the high-level `sdk::Neo` builder.
mod test_provider {
	use std::{iter::Cycle, slice::Iter, sync::Mutex};

	use once_cell::sync::Lazy;

	use super::*;

	// Public endpoints to rotate through to reduce rate limit pressure.
	//
	// These are best-effort defaults intended for local development and examples.
	const MAINNET_ENDPOINTS: &[&str] = &[
		NeoConstants::SEED_1,
		NeoConstants::SEED_2,
		NeoConstants::SEED_3,
		NeoConstants::SEED_4,
		NeoConstants::SEED_5,
	];
	const TESTNET_ENDPOINTS: &[&str] =
		&["https://testnet1.neo.org:443", "https://testnet2.neo.org:443"];

	pub static MAINNET: Lazy<TestProvider> =
		Lazy::new(|| TestProvider::new(MAINNET_ENDPOINTS, "mainnet"));

	pub static TESTNET: Lazy<TestProvider> =
		Lazy::new(|| TestProvider::new(TESTNET_ENDPOINTS, "testnet"));

	#[derive(Debug)]
	pub struct TestProvider {
		network: String,
		endpoints: Mutex<Cycle<Iter<'static, &'static str>>>,
	}

	impl TestProvider {
		pub fn new(endpoints: &'static [&'static str], network: impl Into<String>) -> Self {
			Self { endpoints: endpoints.iter().cycle().into(), network: network.into() }
		}

		pub fn url(&self) -> String {
			let Self { network, endpoints } = self;
			let endpoint = endpoints.lock().unwrap_or_else(|e| e.into_inner()).next().copied();
			match endpoint {
				Some(endpoint) => endpoint.to_string(),
				None => {
					tracing::warn!(
						network = %network,
						"Endpoint list is empty; falling back to default seed URL"
					);
					NeoConstants::SEED_1.to_string()
				},
			}
		}

		pub fn provider(&self) -> RpcClient<Http> {
			let url_str = self.url();
			let url = url::Url::parse(&url_str).unwrap_or_else(|e| {
				tracing::warn!(
					error = %e,
					endpoint = %url_str,
					"Failed to parse endpoint URL; falling back to default seed URL"
				);
				url::Url::parse(NeoConstants::SEED_1)
					.expect("NeoConstants::SEED_1 must be a valid URL")
			});
			let http_provider = Http::new(url).unwrap_or_else(|e| {
				tracing::warn!(
					error = %e,
					"Failed to create HTTP provider; falling back to default seed URL"
				);
				let url = url::Url::parse(NeoConstants::SEED_1)
					.expect("NeoConstants::SEED_1 must be a valid URL");
				Http::new(url).expect("Should be able to create HTTP provider for default seed URL")
			});
			RpcClient::new(http_provider)
		}

		#[cfg(feature = "ws")]
		pub async fn ws(&self) -> Result<RpcClient<Ws>, ProviderError> {
			let url_str = self.url();
			let mut url = url::Url::parse(&url_str).unwrap_or_else(|e| {
				tracing::warn!(
					error = %e,
					endpoint = %url_str,
					"Failed to parse endpoint URL; falling back to default seed URL"
				);
				url::Url::parse(NeoConstants::SEED_1)
					.expect("NeoConstants::SEED_1 must be a valid URL")
			});

			let scheme = match url.scheme() {
				"https" => "wss",
				"http" => "ws",
				"wss" => "wss",
				"ws" => "ws",
				_ => "ws",
			};
			url.set_scheme(scheme).map_err(|_| {
				ProviderError::ParseError(format!(
					"Unsupported URL scheme in endpoint: {}",
					url_str
				))
			})?;
			if url.path() == "/" {
				url.set_path("/ws");
			}

			RpcClient::connect(url.as_str()).await
		}
	}
}
