use std::sync::Arc;
use std::time::Duration;

use electrum_client_netagnostic::{Client, Config, ConfigBuilder, ElectrumApi};
use serde_json::Value;

use super::peer::ServerPeer;
use anyhow::{Context, Result};

// Connection timeout until considering server peer bad
const CANDIDATE_TIMEOUT: Duration = Duration::from_secs(10);

pub struct ElectrumClient {
    client: Option<Arc<Client>>,
}

#[derive(PartialEq, Eq)]
pub enum Protocol {
    TCP,
    SSL,
    ANY,
}

async fn async_call(
    client: Arc<electrum_client_netagnostic::Client>,
    method_name: impl Into<String> + Send + 'static,
    params: impl IntoIterator<Item = electrum_client_netagnostic::Param> + Send + 'static,
) -> Result<Value> {
    let method_name = method_name.into();
    let params_vec: Vec<electrum_client_netagnostic::Param> = params.into_iter().collect();

    Ok(tokio::task::spawn_blocking(move || {
        // Because we use spawn_blocking, the entire call (client, method, params)
        // must be 'static and moved into this closure.
        client.raw_call(&method_name, params_vec)
    })
    .await??)
}

async fn connect(url: String, config: Config) -> Result<Client> {
    Ok(tokio::task::spawn_blocking(move || Client::from_config(&url, config)).await??)
}

impl ElectrumClient {
    async fn connect_ssl(peer: &dyn ServerPeer, config: Config) -> Result<Self> {
        let port = peer.ssl_port().context("SSL not supported by peer")?;

        let client = Arc::new(connect(format!("ssl://{}:{}", peer.ip(), port), config).await?);

        async_call(client.clone(), "server.ping", None)
            .await
            .context("Server did not respond to ping")?;

        Ok(Self {
            client: Some(client),
        })
    }

    async fn connect_tcp(peer: &dyn ServerPeer, config: Config) -> Result<Self> {
        let port = peer.tcp_port().context("TCP not supported by peer")?;

        let client = Arc::new(connect(format!("tcp://{}:{}", peer.ip(), port), config).await?);

        async_call(client.clone(), "server.ping", None)
            .await
            .context("Server did not respond to ping")?;

        Ok(Self {
            client: Some(client),
        })
    }

    fn default_config() -> Config {
        ConfigBuilder::new()
            .timeout(Some(CANDIDATE_TIMEOUT))
            .validate_domain(false)
            .build()
    }

    pub async fn connect(peer: &dyn ServerPeer, protocol: Protocol) -> Result<Self> {
        trace!(
            "discover: Connecting to electrum server {}",
            peer.pretty_hostname()
        );

        if protocol == Protocol::SSL {
            return Self::connect_ssl(peer, Self::default_config()).await;
        }

        if protocol == Protocol::TCP {
            return Self::connect_tcp(peer, Self::default_config()).await;
        }

        debug_assert!(protocol == Protocol::ANY);
        if let Ok(c) = Self::connect_ssl(peer, Self::default_config()).await {
            return Ok(c);
        }
        if let Ok(c) = Self::connect_tcp(peer, Self::default_config()).await {
            return Ok(c);
        }
        bail!("Unable to connec to peer via SSL or TCP");
    }

    pub async fn connect_all(peer: &dyn ServerPeer) -> (Result<Self>, Result<Self>) {
        let ssl = Self::connect_ssl(peer, Self::default_config()).await;
        let tcp = Self::connect_tcp(peer, Self::default_config()).await;
        (ssl, tcp)
    }

    pub async fn call(
        &self,
        method_name: impl Into<String> + Send + 'static,
        params: impl IntoIterator<Item = electrum_client_netagnostic::Param> + Send + 'static,
    ) -> Result<Value> {
        return async_call(
            self.client.as_ref().context("Not connected")?.clone(),
            method_name,
            params,
        )
        .await;
    }
}
