//! A [`TunnelScaffolding`] implementation for the Apple platform.

use crate::ffi_event::{RelayDetails, TunnelEvent};
use crate::OnionmasqApple;
use futures::Stream;
use onion_tunnel::accounting::BandwidthCounter;
use onion_tunnel::scaffolding::{ConnectionDetails, FailedConnectionDetails, TunnelCommand};
use onion_tunnel::{CountryCode, IpEndpoint, TunnelScaffolding};
use std::collections::HashMap;
use std::io;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex, RwLock};
use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream;
use tracing::warn;

pub struct AppleScaffolding {
    /// This is only mpsc so it can be Sync.
    pub(crate) bootstrap_done: mpsc::UnboundedSender<()>,
    pub(crate) command_receiver: Mutex<Option<mpsc::UnboundedReceiver<TunnelCommand>>>,
    pub(crate) counters: super::BandwidthCounterMap,
    pub(crate) country_code: Arc<RwLock<Option<CountryCode>>>,
}

impl AppleScaffolding {
    fn on_tunnel_event(evt: TunnelEvent) {
        if let Err(e) = OnionmasqApple::get().on_tunnel_event(evt) {
            warn!("Failed to communicate tunnel event: {e}");
        }
    }
}

impl TunnelScaffolding for AppleScaffolding {
    fn locate(&self, _: IpEndpoint, _: IpEndpoint, _: u64) -> Option<CountryCode> {
        *self.country_code.read().unwrap()
    }

    fn on_bootstrapped(&self) {
        let _ = self.bootstrap_done.send(());
    }

    fn on_directory(&self, relays_by_country: HashMap<String, usize>) {
        Self::on_tunnel_event(TunnelEvent::NewDirectory { relays_by_country })
    }

    fn on_established(&self, details: ConnectionDetails<'_>) {
        Self::on_tunnel_event(TunnelEvent::NewConnection {
            proxy_src: SocketAddr::new(details.proxy_src.addr.into(), details.proxy_src.port),
            proxy_dst: SocketAddr::new(details.proxy_dst.addr.into(), details.proxy_dst.port),
            tor_dst: details.tor_dst.to_string(),
            app_id: details.isolation_key,
            circuit: details
                .circuit_relays()
                .into_iter()
                .map(|x| RelayDetails {
                    rsa_identity: x.rsa_identity.map(|x| x.to_string()),
                    ed_identity: x.ed_identity.map(|x| x.to_string()),
                    addresses: x.addresses,
                    country_code: x.country_code.map(|x| x.to_string()),
                })
                .collect(),
        });
    }

    fn on_arti_failure(&self, details: FailedConnectionDetails) {
        Self::on_tunnel_event(TunnelEvent::FailedConnection {
            proxy_src: SocketAddr::new(details.proxy_src.addr.into(), details.proxy_src.port),
            proxy_dst: SocketAddr::new(details.proxy_dst.addr.into(), details.proxy_dst.port),
            tor_dst: details.tor_dst.to_string(),
            app_id: details.isolation_key,
            error: details.error.to_string(),
        })
    }

    fn on_socket_close(&self, src: IpEndpoint, dst: IpEndpoint, err: Option<io::Error>) {
        Self::on_tunnel_event(TunnelEvent::ClosedConnection {
            proxy_src: SocketAddr::new(src.addr.into(), src.port),
            proxy_dst: SocketAddr::new(dst.addr.into(), dst.port),
            error: err.map(|x| x.to_string()),
        })
    }

    fn command_stream(&self) -> Option<Box<dyn Stream<Item = TunnelCommand> + Send + Sync>> {
        if let Some(rx) = self.command_receiver.lock().unwrap().take() {
            Some(Box::new(UnboundedReceiverStream::new(rx)))
        } else {
            panic!("AppleScaffolding::command_stream() called more than once");
        }
    }

    fn get_bandwidth_counter(&self, isolation_key: u64) -> Option<Arc<BandwidthCounter>> {
        Some(
            self.counters
                .entry(isolation_key)
                .or_insert_with(|| Arc::new(BandwidthCounter::new()))
                .clone(),
        )
    }
}
