use crate::acns::ACNError;
use crate::acns::ACNError::ConfigurationError;
use std::fs::File;
use std::io::Write;

/// Build a Torrc File with a set of configured options. Note: This is not a complete Torrc builder
/// although it might one day grow to be one. As it stands it only allows the configuration options
/// most necessary for Tor v3 Onion Service clients/servers as they related to Tapir/Cwtch.
/// We use the Builder patter to allow easy programmatic construction for different Tor configurations.
pub struct TorrcGenerator {
    socks_port: u16,
    control_port: u16,
    hashed_password: String,
}

impl TorrcGenerator {
    /// Generate a new, empty Torrc Generator.
    pub fn new() -> TorrcGenerator {
        TorrcGenerator {
            socks_port: 0,
            control_port: 0,
            hashed_password: String::new(),
        }
    }

    /// Configure Tor to open a port to connect to external servers over Tor via SOCKS5
    pub fn with_socks_port(mut self, socks_port: u16) -> Self {
        self.socks_port = socks_port;
        self
    }

    /// Configure Tor to have an open Control Port for configuration.
    pub fn with_control_port(mut self, control_port: u16) -> Self {
        self.control_port = control_port;
        self
    }

    /// Force the tor control port to accept a hashed password as authentication, the given password
    /// is hashed with a random salt
    /// From the Tor Docs: If the 'HashedControlPassword' option is set, it must contain the salted
    /// hash of a secret password.  The salted hash is computed according to the
    /// S2K algorithm in RFC 2440 (OpenPGP), and prefixed with the s2k specifier.
    /// This is then encoded in hexadecimal, prefixed by the indicator sequence
    /// "16:".
    pub fn with_hashed_control_password(mut self, password: &str) -> Self {
        let salt: [u8; 8] = rand::random();
        self.hashed_password = TorrcGenerator::generate_hashed_password(&salt, password);
        self
    }

    /// generate_hashed_password calculates a hash in the same way tha tor --hash-password does
    /// this function takes a salt as input which is not great from an api-misuse perspective, but
    /// we make it private.
    fn generate_hashed_password(salt: &[u8; 8], password: &str) -> String {
        let c = 96;
        let mut count = (16 + (c & 15)) << ((c >> 4) + 6);
        let mut tmp = vec![];
        tmp.extend_from_slice(salt);
        tmp.extend_from_slice(password.as_bytes());
        let slen = tmp.len();
        let mut hash = sha1::Sha1::new();
        while count != 0 {
            if count > slen {
                hash.update(tmp.as_slice());
                count -= slen
            } else {
                let tmp_slice = tmp.as_slice();
                hash.update(tmp_slice.split_at(count).0);
                count = 0
            }
        }
        let hashed = hash.digest().bytes();
        return format!("16:{}{:X}{}", hex::encode_upper(salt), c, hex::encode_upper(hashed));
    }

    /// Compile the final torrc file and write it to the file system.
    pub fn build(&self, file_name: &str) -> Result<(), ACNError> {
        let mut file = match File::create(file_name) {
            Err(why) => return Err(ConfigurationError(why.to_string())),
            Ok(file) => file,
        };
        let mut lines = vec![];

        if self.socks_port != 0 {
            lines.push(format!("SocksPort {}", self.socks_port));
        }

        if self.control_port != 0 {
            lines.push(format!("ControlPort {}", self.control_port));
        }

        if self.hashed_password.is_empty() == false {
            lines.push(format!("HashedControlPassword {}", self.hashed_password));
        }

        match file.write_all(lines.join("\n").as_bytes()) {
            Ok(_) => Ok(()),
            Err(why) => Err(ConfigurationError(why.to_string())),
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::acns::tor::torrc::TorrcGenerator;
    use std::fs::{read_to_string, remove_file};
    use std::path::Path;

    #[test]
    fn generate_hashed_password() {
        let salt: [u8; 8] = [0xC1, 0x53, 0x05, 0xF9, 0x77, 0x89, 0x41, 0x4B];
        assert_eq!(
            String::from("16:C15305F97789414B601259E3EC5E76B8E55FC56A9F562B713F3D2BA257"),
            TorrcGenerator::generate_hashed_password(&salt, "examplehashedpassword")
        );
    }

    #[test]
    fn generate_torrc() {
        let gen = TorrcGenerator::new().with_control_port(9051).with_hashed_control_password("examplehashedpassword");
        match gen.build("./torrc") {
            Ok(()) => {
                assert!(Path::new("./torrc").exists());
                println!("{}", read_to_string("./torrc").unwrap());
                assert!(remove_file("./torrc").is_ok());
            }
            _ => assert!(false),
        }
    }
}
