// Copyright 2021. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

use std::{fmt, fs, io, net::TcpListener, path::PathBuf, thread};

use derivative::Derivative;
use libtor::{LogDestination, LogLevel, TorFlag};
use log::*;
use rand::{RngExt, distr::Alphanumeric};
use tari_common::exit_codes::{ExitCode, ExitError};
use tari_p2p::{TorControlAuthentication, TransportConfig};
use tor_hash_passwd::EncryptedKey;

const LOG_TARGET: &str = "tari_libtor";

pub struct TorPassword(Option<String>);

impl fmt::Debug for TorPassword {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "TorPassword: ...")
    }
}

#[derive(Derivative)]
#[derivative(Debug)]
pub struct Tor {
    control_port: u16,
    data_dir: PathBuf,
    log_destination: PathBuf,
    log_level: LogLevel,
    #[derivative(Debug = "ignore")]
    passphrase: TorPassword,
    socks_port: u16,
}

impl Default for Tor {
    fn default() -> Tor {
        Tor {
            control_port: 0,
            data_dir: "/tmp/tor-data".into(),
            log_destination: "/tmp/tor.log".into(),
            log_level: LogLevel::Err,
            passphrase: TorPassword(None),
            socks_port: 0,
        }
    }
}

impl Tor {
    /// Returns a new Tor instance with random options.
    /// The data directory, passphrase, and log destination are temporary and randomized.
    /// Two TCP ports will be provided by the operating system.
    /// These ports are used for the control and socks ports, the onion address and port info are still loaded from the
    /// node identity file.
    pub fn initialize(data_dir: PathBuf) -> Result<Tor, ExitError> {
        debug!(target: LOG_TARGET, "Initializing libtor");
        let mut instance = Tor::default();

        // check for unused ports to assign
        let (socks_port, control_port) = get_available_ports()?;
        debug!(target: LOG_TARGET, "Using socks port {socks_port} and control_port {control_port}");
        instance.socks_port = socks_port;
        instance.control_port = control_port;

        // generate a random passphrase
        let passphrase: String = rand::rng()
            .sample_iter(&Alphanumeric)
            .take(30)
            .map(char::from)
            .collect();
        instance.passphrase = TorPassword(Some(passphrase));

        // data dir
        instance.data_dir = data_dir.join("data");

        let exists = fs::exists(instance.data_dir.clone()).map_err(|e| {
            ExitError::new(
                ExitCode::InputError,
                format!(
                    "Could not verify libtor data directory: {} ({})",
                    instance.data_dir.display(),
                    e
                ),
            )
        })?;
        if !exists {
            fs::create_dir_all(&instance.data_dir).map_err(|e| {
                ExitError::new(
                    ExitCode::InputError,
                    format!(
                        "Could not create libtor data directory: {} ({})",
                        instance.data_dir.display(),
                        e
                    ),
                )
            })?;
        }

        // log destination
        instance.log_destination = data_dir.join("tor.log");

        debug!(target: LOG_TARGET, "tor instance: {instance:?}");
        Ok(instance)
    }

    /// Override a given Tor comms transport with the control address and auth from this instance
    pub fn update_comms_transport(&self, transport: &mut TransportConfig) -> Result<(), ExitError> {
        if !transport.transport_type.uses_tor_hidden_service() {
            let e = format!("Expected a TorHiddenService comms transport, received: {transport:?}");
            return Err(ExitError::new(ExitCode::ConfigError, e));
        }

        if let Some(ref passphrase) = self.passphrase.0 {
            transport.tor.control_auth = TorControlAuthentication::Password(passphrase.to_owned());
        }
        transport.tor.control_address = format!("/ip4/127.0.0.1/tcp/{}", self.control_port).parse().unwrap();
        debug!(target: LOG_TARGET, "updated comms transport: {transport:?}");
        Ok(())
    }

    /// Run the Tor instance in the background and return a handle to the thread.
    pub fn run_background(self) -> thread::JoinHandle<Result<u8, libtor::Error>> {
        info!(target: LOG_TARGET, "Starting Tor instance");

        let Tor {
            data_dir,
            socks_port,
            control_port,
            log_level,
            log_destination,
            passphrase,
            ..
        } = self;

        let mut tor = libtor::Tor::new();

        tor.flag(TorFlag::DataDirectory(data_dir.to_string_lossy().to_string()))
            // Disable signal handlers so that ctrl+c can be handled by our application
            // https://github.com/torproject/torspec/blob/8961bb4d83fccb2b987f9899ca83aa430f84ab0c/control-spec.txt#L3946
            .flag(TorFlag::Custom("__DisableSignalHandlers 1".to_string()))
            // Prevent conflicts with multiple instances using the same listener port for Prometheus metrics
            .flag(TorFlag::Custom("MetricsPort 0".to_string()))
            // Write the final control port to a file. This could be used to configure the node to use this port when auto is set.
            .flag(TorFlag::ControlPortWriteToFile(data_dir.join("control_port").to_string_lossy().to_string()))
            .flag(TorFlag::Hush())
            .flag(TorFlag::LogTo(log_level, LogDestination::File(log_destination.to_string_lossy().to_string())));

        if socks_port == 0 {
            tor.flag(TorFlag::SocksPortAuto);
        } else {
            tor.flag(TorFlag::SocksPort(socks_port));
        }

        if control_port == 0 {
            tor.flag(TorFlag::ControlPortAuto);
        } else {
            tor.flag(TorFlag::ControlPort(control_port));
        }

        if let Some(secret) = passphrase.0 {
            let hash = EncryptedKey::hash_password(&secret).to_string();
            tor.flag(TorFlag::HashedControlPassword(hash));
        }

        tor.start_background()
    }
}

/// Attempt to find 2 available TCP ports
fn get_available_ports() -> Result<(u16, u16), io::Error> {
    let localhost = "127.0.0.1";
    let listener1 = TcpListener::bind((localhost, 0))?;
    let port1 = listener1.local_addr()?.port();

    let listener2 = TcpListener::bind((localhost, 0))?;
    let port2 = listener2.local_addr()?.port();

    Ok((port1, port2))
}
