use std::{
    ops::{Deref, DerefMut},
    sync::Arc,
};

pub use arti_client;
pub use safelog::DisplayRedacted;
pub use tor_cell;
pub use tor_hsservice;

use snafu::OptionExt;
use snafu::prelude::*;

use arti_client::{DataStream, TorClientConfig};
use futures_util::{Stream, StreamExt};
use tor_cell::relaycell::msg::Connected;
use tor_hsservice::{RendRequest, StreamRequest};

pub type TorClient = arti_client::TorClient<tor_rtcompat::PreferredRuntime>;
pub type TorClientBuilder = arti_client::TorClientBuilder<tor_rtcompat::PreferredRuntime>;

#[cfg(feature = "http-client")]
mod http_client;

#[cfg(feature = "http-client")]
pub use http_client::{HttpClient, HttpClientBuilder};

#[cfg(feature = "http-server")]
mod http_server;

#[cfg(feature = "http-server")]
pub use http_server::HttpServer;

mod error;
pub use error::*;

pub type Result<T, E = Error> = std::result::Result<T, E>;

/// Connection to the Tor network
#[derive(Clone)]
pub struct Tor {
    /// Arti Tor client
    pub client: TorClient,
}

impl Deref for Tor {
    type Target = TorClient;

    fn deref(&self) -> &Self::Target {
        &self.client
    }
}

impl DerefMut for Tor {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.client
    }
}

/// Used to configure connections to the Tor network, in most cases you will want to bootstrap on startup,
/// but there are cases where you might want to delay bootrstrapping since it does take time.
#[derive(Debug, Clone)]
pub struct TorConfig {
    /// Boostrap Tor network connection, if set to false you will need to manually bootstrap at another point
    pub bootstrap: bool,

    // Tor client config
    pub client_config: Option<TorClientConfig>,
}

impl Default for TorConfig {
    fn default() -> Self {
        TorConfig {
            bootstrap: true,
            client_config: None,
        }
    }
}

impl TorConfig {
    /// Create a new config with default values, this sets `bootstrap=true`
    pub fn new() -> Self {
        Self::default()
    }

    /// Enable/disable bootstrapping
    pub fn with_bootstrap(mut self, bootstrap: bool) -> Self {
        self.bootstrap = bootstrap;
        self
    }

    /// Set orclear client config
    pub fn with_client_config(mut self, config: Option<TorClientConfig>) -> Self {
        self.client_config = config;
        self
    }
}

impl Tor {
    /// Create a new connection to the Tor network using the provided configuration options
    #[tracing::instrument(err, level = "info")]
    pub async fn new(config: TorConfig) -> Result<Self, Error> {
        let client = if config.bootstrap {
            Self::new_custom(async move |b| {
                b.config(config.client_config.unwrap_or_default())
                    .create_bootstrapped()
                    .await
                    .context(ArtiClientSnafu)
            })
            .await?
        } else {
            Self::new_custom(async move |b| {
                b.config(config.client_config.unwrap_or_default())
                    .create_unbootstrapped_async()
                    .await
                    .context(ArtiClientSnafu)
            })
            .await?
        };

        Ok(client)
    }

    /// Create a new connection to the Tor network using a callback to edit a `TorClientBuilder` instance
    #[tracing::instrument(err, skip(callback), level = "info")]
    pub async fn new_custom<
        T: Future<Output = Result<TorClient, Error>>,
        F: FnOnce(TorClientBuilder) -> T,
    >(
        callback: F,
    ) -> Result<Self, Error> {
        let builder = arti_client::TorClient::builder();
        let client = callback(builder).await?;
        Ok(Self { client })
    }

    /// Start a new Tor Hidden Service with the given identifier, this identifier is used to associate the service and its keys
    pub fn service(&self, name: &str) -> Result<HiddenService> {
        HiddenService::new(self, name)
    }
}

/// Tor Hidden Service
pub struct HiddenService {
    tor: Tor,
    service: Arc<tor_hsservice::RunningOnionService>,
    stream: std::pin::Pin<Box<dyn Send + Sync + futures_util::Stream<Item = RendRequest>>>,
}

impl HiddenService {
    /// Create a new Tor Hidden Service with the given identifier
    #[tracing::instrument(skip(tor), err)]
    pub fn new(tor: &Tor, name: &str) -> Result<Self> {
        let service = tor_hsservice::config::OnionServiceConfigBuilder::default()
            .nickname(name.parse().context(InvalidNicknameSnafu)?)
            .build()
            .context(ConfigBuildSnafu)?;

        let (service, stream) = tor
            .client
            .launch_onion_service(service)
            .context(ArtiClientSnafu)?
            .context(EmptyOnionServiceSnafu {
                name: name.to_owned(),
            })?;

        Ok(HiddenService {
            tor: tor.clone(),
            service,
            stream: Box::pin(stream),
        })
    }

    /// Access underlying tor connection
    pub fn tor(&self) -> &Tor {
        &self.tor
    }

    /// Accept an incoming Tor connection
    #[tracing::instrument(skip(self), err)]
    pub async fn accept(&mut self) -> Result<Option<TorConnection>> {
        if let Some(x) = self.stream.next().await {
            x.accept()
                .await
                .map_err(Box::new)
                .context(ClientSnafu)
                .map(|stream| {
                    Option::Some(TorConnection {
                        stream: Box::new(stream),
                    })
                })
        } else {
            Ok(None)
        }
    }

    /// Accept incoming tor connection and open incoming client
    #[tracing::instrument(skip(self), err)]
    pub async fn handle(&mut self) -> std::io::Result<Option<(Address, DataStream)>> {
        if let Some(x) = self.stream.next().await
            && let Some(mut conn) = x
                .accept()
                .await
                .map_err(|_| {
                    std::io::Error::new(
                        std::io::ErrorKind::NotConnected,
                        "unable to accept connection",
                    )
                })
                .map(|stream| {
                    Option::Some(TorConnection {
                        stream: Box::new(stream),
                    })
                })?
        {
            return conn.accept_next().await.map_err(|_| {
                std::io::Error::new(
                    std::io::ErrorKind::NotConnected,
                    "unable to open data stream",
                )
            });
        }
        Ok(None)
    }
}

impl Deref for HiddenService {
    type Target = tor_hsservice::RunningOnionService;

    fn deref(&self) -> &Self::Target {
        &self.service
    }
}

/// An incoming Tor connection stream, this is used to accept requests from incoming clients
pub struct TorConnection {
    stream: Box<dyn 'static + Send + Stream<Item = StreamRequest> + Unpin>,
}

impl TorConnection {
    /// Accept next incoming stream
    #[tracing::instrument(skip(self), err)]
    pub async fn accept_next(&mut self) -> Result<Option<(Address, DataStream)>> {
        if let Some(conn) = self.stream.next().await {
            let addr = match conn.request() {
                tor_proto::client::stream::IncomingStreamRequest::Begin(begin) => Address {
                    addr: begin.addr().to_vec(),
                    port: begin.port(),
                },
                _ => {
                    conn.shutdown_circuit().context(ArtiBugSnafu)?;
                    return Ok(None);
                }
            };

            let conn = conn
                .accept(Connected::new_empty())
                .await
                .map_err(Box::new)
                .context(ClientSnafu)?;
            return Ok(Some((addr, conn)));
        }

        Ok(None)
    }
}

/// IP address for Tor connections
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Address {
    addr: Vec<u8>,
    port: u16,
}

impl Stream for TorConnection {
    type Item = StreamRequest;

    fn poll_next(
        mut self: std::pin::Pin<&mut Self>,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Option<Self::Item>> {
        self.stream.poll_next_unpin(cx)
    }
}
