use std::ffi::CString;
use std::io::Write;
use std::mem;
use std::path::Path;
use std::process::{Command, Stdio};
use std::ptr;
use url;

use {raw, Error, Config, IntoCString};
use util::Binding;

/// A structure to represent git credentials in libgit2.
pub struct Cred {
    raw: *mut raw::git_cred,
}

/// Management of the gitcredentials(7) interface.
pub struct CredentialHelper {
    /// A public field representing the currently discovered username from
    /// configuration.
    pub username: Option<String>,
    protocol: Option<String>,
    host: Option<String>,
    url: String,
    commands: Vec<String>,
}

impl Cred {
    /// Create a "default" credential usable for Negotiate mechanisms like NTLM
    /// or Kerberos authentication.
    pub fn default() -> Result<Cred, Error> {
        ::init();
        let mut out = ptr::null_mut();
        unsafe {
            try_call!(raw::git_cred_default_new(&mut out));
            Ok(Binding::from_raw(out))
        }
    }

    /// Create a new ssh key credential object used for querying an ssh-agent.
    ///
    /// The username specified is the username to authenticate.
    pub fn ssh_key_from_agent(username: &str) -> Result<Cred, Error> {
        ::init();
        let mut out = ptr::null_mut();
        let username = try!(CString::new(username));
        unsafe {
            try_call!(raw::git_cred_ssh_key_from_agent(&mut out, username));
            Ok(Binding::from_raw(out))
        }
    }

    /// Create a new passphrase-protected ssh key credential object.
    pub fn ssh_key(username: &str,
                   publickey: Option<&Path>,
                   privatekey: &Path,
                   passphrase: Option<&str>) -> Result<Cred, Error> {
        ::init();
        let username = try!(CString::new(username));
        let publickey = try!(::opt_cstr(publickey));
        let privatekey = try!(privatekey.into_c_string());
        let passphrase = try!(::opt_cstr(passphrase));
        let mut out = ptr::null_mut();
        unsafe {
            try_call!(raw::git_cred_ssh_key_new(&mut out, username, publickey,
                                                privatekey, passphrase));
            Ok(Binding::from_raw(out))
        }
    }

    /// Create a new ssh key credential object reading the keys from memory.
    pub fn ssh_key_from_memory(username: &str,
                               publickey: Option<&str>,
                               privatekey: &str,
                               passphrase: Option<&str>) -> Result<Cred, Error> {
        ::init();
        let username = try!(CString::new(username));
        let publickey = try!(::opt_cstr(publickey));
        let privatekey = try!(CString::new(privatekey));
        let passphrase = try!(::opt_cstr(passphrase));
        let mut out = ptr::null_mut();
        unsafe {
            try_call!(raw::git_cred_ssh_key_memory_new(&mut out, username, publickey,
                                                       privatekey, passphrase));
            Ok(Binding::from_raw(out))
        }
    }

    /// Create a new plain-text username and password credential object.
    pub fn userpass_plaintext(username: &str,
                              password: &str) -> Result<Cred, Error> {
        ::init();
        let username = try!(CString::new(username));
        let password = try!(CString::new(password));
        let mut out = ptr::null_mut();
        unsafe {
            try_call!(raw::git_cred_userpass_plaintext_new(&mut out, username,
                                                           password));
            Ok(Binding::from_raw(out))
        }
    }

    /// Attempt to read `credential.helper` according to gitcredentials(7) [1]
    ///
    /// This function will attempt to parse the user's `credential.helper`
    /// configuration, invoke the necessary processes, and read off what the
    /// username/password should be for a particular url.
    ///
    /// The returned credential type will be a username/password credential if
    /// successful.
    ///
    /// [1]: https://www.kernel.org/pub/software/scm/git/docs/gitcredentials.html
    pub fn credential_helper(config: &Config,
                             url: &str,
                             username: Option<&str>)
                             -> Result<Cred, Error> {
        match CredentialHelper::new(url).config(config).username(username)
                               .execute() {
            Some((username, password)) => {
                Cred::userpass_plaintext(&username, &password)
            }
            None => Err(Error::from_str("failed to acquire username/password \
                                         from local configuration"))
        }
    }

    /// Create a credential to specify a username.
    ///
    /// THis is used with ssh authentication to query for the username if non is
    /// specified in the url.
    pub fn username(username: &str) -> Result<Cred, Error> {
        ::init();
        let username = try!(CString::new(username));
        let mut out = ptr::null_mut();
        unsafe {
            try_call!(raw::git_cred_username_new(&mut out, username));
            Ok(Binding::from_raw(out))
        }
    }

    /// Check whether a credential object contains username information.
    pub fn has_username(&self) -> bool {
        unsafe { raw::git_cred_has_username(self.raw) == 1 }
    }

    /// Return the type of credentials that this object represents.
    pub fn credtype(&self) -> raw::git_credtype_t {
        unsafe { (*self.raw).credtype }
    }

    /// Unwrap access to the underlying raw pointer, canceling the destructor
    pub unsafe fn unwrap(mut self) -> *mut raw::git_cred {
        mem::replace(&mut self.raw, ptr::null_mut())
    }
}

impl Binding for Cred {
    type Raw = *mut raw::git_cred;

    unsafe fn from_raw(raw: *mut raw::git_cred) -> Cred {
        Cred { raw: raw }
    }
    fn raw(&self) -> *mut raw::git_cred { self.raw }
}

impl Drop for Cred {
    fn drop(&mut self) {
        if !self.raw.is_null() {
            unsafe { ((*self.raw).free)(self.raw) }
        }
    }
}

impl CredentialHelper {
    /// Create a new credential helper object which will be used to probe git's
    /// local credential configuration.
    ///
    /// The url specified is the namespace on which this will query credentials.
    /// Invalid urls are currently ignored.
    pub fn new(url: &str) -> CredentialHelper {
        let mut ret = CredentialHelper {
            protocol: None,
            host: None,
            username: None,
            url: url.to_string(),
            commands: Vec::new(),
        };

        // Parse out the (protocol, host) if one is available
        if let Ok(url) = url::Url::parse(url) {
            if let Some(url::Host::Domain(s)) = url.host() {
                ret.host = Some(s.to_string());
            }
            ret.protocol = Some(url.scheme().to_string())
        }
        ret
    }

    /// Set the username that this credential helper will query with.
    ///
    /// By default the username is `None`.
    pub fn username(&mut self, username: Option<&str>) -> &mut CredentialHelper {
        self.username = username.map(|s| s.to_string());
        self
    }

    /// Query the specified configuration object to discover commands to
    /// execute, usernames to query, etc.
    pub fn config(&mut self, config: &Config) -> &mut CredentialHelper {
        // Figure out the configured username/helper program.
        //
        // see http://git-scm.com/docs/gitcredentials.html#_configuration_options
        //
        // TODO: implement useHttpPath
        if self.username.is_none() {
            self.config_username(config);
        }
        self.config_helper(config);
        self
    }

    // Configure the queried username from `config`
    fn config_username(&mut self, config: &Config) {
        let key = self.exact_key("username");
        self.username = config.get_string(&key).ok().or_else(|| {
            self.url_key("username").and_then(|s| {
                config.get_string(&s).ok()
            })
        }).or_else(|| {
            config.get_string("credential.username").ok()
        })
    }

    // Discover all `helper` directives from `config`
    fn config_helper(&mut self, config: &Config) {
        let exact = config.get_string(&self.exact_key("helper"));
        self.add_command(exact.as_ref().ok().map(|s| &s[..]));
        if let Some(key) = self.url_key("helper") {
            let url = config.get_string(&key);
            self.add_command(url.as_ref().ok().map(|s| &s[..]));
        }
        let global = config.get_string("credential.helper");
        self.add_command(global.as_ref().ok().map(|s| &s[..]));
    }

    // Add a `helper` configured command to the list of commands to execute.
    //
    // see https://www.kernel.org/pub/software/scm/git/docs/technical
    //                           /api-credentials.html#_credential_helpers
    fn add_command(&mut self, cmd: Option<&str>) {
        let cmd = match cmd {
            Some("") | None => return,
            Some(s) => s,
        };

        if cmd.starts_with('!') {
            self.commands.push(cmd[1..].to_string());
        } else if cmd.starts_with('/') || cmd.starts_with('\\') ||
                  cmd[1..].starts_with(":\\") {
            self.commands.push(format!("\"{}\"", cmd));
        } else {
            self.commands.push(format!("git credential-{}", cmd));
        }
    }

    fn exact_key(&self, name: &str) -> String {
        format!("credential.{}.{}", self.url, name)
    }

    fn url_key(&self, name: &str) -> Option<String> {
        match (&self.host, &self.protocol) {
            (&Some(ref host), &Some(ref protocol)) => {
                Some(format!("credential.{}://{}.{}", protocol, host, name))
            }
            _ => None
        }
    }

    /// Execute this helper, attempting to discover a username/password pair.
    ///
    /// All I/O errors are ignored, (to match git behavior), and this function
    /// only succeeds if both a username and a password were found
    pub fn execute(&self) -> Option<(String, String)> {
        let mut username = self.username.clone();
        let mut password = None;
        for cmd in &self.commands {
            let (u, p) = self.execute_cmd(cmd, &username);
            if u.is_some() && username.is_none() {
                username = u;
            }
            if p.is_some() && password.is_none() {
                password = p;
            }
            if username.is_some() && password.is_some() { break }
        }

        match (username, password) {
            (Some(u), Some(p)) => Some((u, p)),
            _ => None,
        }
    }

    // Execute the given `cmd`, providing the appropriate variables on stdin and
    // then afterwards parsing the output into the username/password on stdout.
    fn execute_cmd(&self, cmd: &str, username: &Option<String>)
                   -> (Option<String>, Option<String>) {
        macro_rules! my_try( ($e:expr) => (
            match $e {
                Ok(e) => e,
                Err(e) => {
                    debug!("{} failed with {}", stringify!($e), e);
                    return (None, None)
                }
            }
        ) );

        // It looks like the `cmd` specification is typically bourne-shell-like
        // syntax, so try that first. If that fails, though, we may be on a
        // Windows machine for example where `sh` isn't actually available by
        // default. Most credential helper configurations though are pretty
        // simple (aka one or two space-separated strings) so also try to invoke
        // the process directly.
        //
        // If that fails then it's up to the user to put `sh` in path and make
        // sure it works.
        let mut c = Command::new("sh");
        c.arg("-c")
            .arg(&format!("{} get", cmd))
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped());
        let mut p = match c.spawn() {
            Ok(p) => p,
            Err(e) => {
                debug!("`sh` failed to spawn: {}", e);
                let mut parts = cmd.split_whitespace();
                let mut c = Command::new(parts.next().unwrap());
                for arg in parts {
                    c.arg(arg);
                }
                c.arg("get")
                    .stdin(Stdio::piped())
                    .stdout(Stdio::piped())
                    .stderr(Stdio::piped());
                match c.spawn() {
                    Ok(p) => p,
                    Err(e) => {
                        debug!("fallback of {:?} failed with {}", cmd, e);
                        return (None, None);
                    }
                }
            }
        };

        // Ignore write errors as the command may not actually be listening for
        // stdin
        {
            let stdin = p.stdin.as_mut().unwrap();
            if let Some(ref p)  = self.protocol {
                let _ = writeln!(stdin, "protocol={}", p);
            }
            if let Some(ref p)  = self.host {
                let _ = writeln!(stdin, "host={}", p);
            }
            if let Some(ref p)  = *username {
                let _ = writeln!(stdin, "username={}", p);
            }
        }
        let output = my_try!(p.wait_with_output());
        if !output.status.success() {
            debug!("credential helper failed: {}\nstdout ---\n{}\nstdout ---\n{}",
                   output.status,
                   String::from_utf8_lossy(&output.stdout),
                   String::from_utf8_lossy(&output.stderr));
            return (None, None)
        }
        trace!("credential helper stderr ---\n{}",
               String::from_utf8_lossy(&output.stderr));
        self.parse_output(output.stdout)
    }

    // Parse the output of a command into the username/password found
    fn parse_output(&self, output: Vec<u8>) -> (Option<String>, Option<String>) {
        // Parse the output of the command, looking for username/password
        let mut username = None;
        let mut password = None;
        for line in output.split(|t| *t == b'\n') {
            let mut parts = line.splitn(2, |t| *t == b'=');
            let key = parts.next().unwrap();
            let value = match parts.next() {
                Some(s) => s,
                None => {
                    trace!("ignoring output line: {}", String::from_utf8_lossy(line));
                    continue
                }
            };
            let value = match String::from_utf8(value.to_vec()) {
                Ok(s) => s,
                Err(..) => continue,
            };
            match key {
                b"username" => username = Some(value),
                b"password" => password = Some(value),
                _ => {}
            }
        }
        (username, password)
    }
}

#[cfg(all(test, feature = "unstable"))]
mod test {
    use std::env;
    use std::fs::File;
    use std::io::prelude::*;
    use std::path::Path;
    use tempdir::TempDir;

    use {Cred, Config, CredentialHelper, ConfigLevel};

    macro_rules! test_cfg( ($($k:expr => $v:expr),*) => ({
        let td = TempDir::new("git2-rs").unwrap();
        let mut cfg = Config::new().unwrap();
        cfg.add_file(&td.path().join("cfg"), ConfigLevel::Highest, false).unwrap();
        $(cfg.set_str($k, $v).unwrap();)*
        cfg
    }) );

    #[test]
    fn smoke() {
        Cred::default().unwrap();
    }

    #[test]
    fn credential_helper1() {
        let cfg = test_cfg! {
            "credential.helper" => "!f() { echo username=a; echo password=b; }; f"
        };
        let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
                                      .config(&cfg)
                                      .execute().unwrap();
        assert_eq!(u, "a");
        assert_eq!(p, "b");
    }

    #[test]
    fn credential_helper2() {
        let cfg = test_cfg! {};
        assert!(CredentialHelper::new("https://example.com/foo/bar")
                                 .config(&cfg)
                                 .execute().is_none());
    }

    #[test]
    fn credential_helper3() {
        let cfg = test_cfg! {
            "credential.https://example.com.helper" =>
                    "!f() { echo username=c; }; f",
            "credential.helper" => "!f() { echo username=a; echo password=b; }; f"
        };
        let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
                                      .config(&cfg)
                                      .execute().unwrap();
        assert_eq!(u, "c");
        assert_eq!(p, "b");
    }

    #[test]
    fn credential_helper4() {
        let td = TempDir::new("git2-rs").unwrap();
        let path = td.path().join("script");
        File::create(&path).unwrap().write(br"\
#!/bin/sh
echo username=c
").unwrap();
        chmod(&path);
        let cfg = test_cfg! {
            "credential.https://example.com.helper" =>
                    &path.display().to_string()[..],
            "credential.helper" => "!f() { echo username=a; echo password=b; }; f"
        };
        let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
                                      .config(&cfg)
                                      .execute().unwrap();
        assert_eq!(u, "c");
        assert_eq!(p, "b");
    }

    #[test]
    fn credential_helper5() {
        let td = TempDir::new("git2-rs").unwrap();
        let path = td.path().join("git-credential-script");
        File::create(&path).unwrap().write(br"\
#!/bin/sh
echo username=c
").unwrap();
        chmod(&path);

        let paths = env::var("PATH").unwrap();
        let paths = env::split_paths(&paths)
                        .chain(path.parent().map(|p| p.to_path_buf()).into_iter());
        env::set_var("PATH", &env::join_paths(paths).unwrap());

        let cfg = test_cfg! {
            "credential.https://example.com.helper" => "script",
            "credential.helper" => "!f() { echo username=a; echo password=b; }; f"
        };
        let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
                                      .config(&cfg)
                                      .execute().unwrap();
        assert_eq!(u, "c");
        assert_eq!(p, "b");
    }

    #[test]
    fn credential_helper6() {
        let cfg = test_cfg! {
            "credential.helper" => ""
        };
        assert!(CredentialHelper::new("https://example.com/foo/bar")
                .config(&cfg)
                .execute().is_none());
    }

    #[test]
    fn ssh_key_from_memory() {
        let cred = Cred::ssh_key_from_memory(
            "test",
            Some("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDByAO8uj+kXicj6C2ODMspgmUoVyl5eaw8vR6a1yEnFuJFzevabNlN6Ut+CPT3TRnYk5BW73pyXBtnSL2X95BOnbjMDXc4YIkgs3YYHWnxbqsD4Pj/RoGqhf+gwhOBtL0poh8tT8WqXZYxdJQKLQC7oBqf3ykCEYulE4oeRUmNh4IzEE+skD/zDkaJ+S1HRD8D8YCiTO01qQnSmoDFdmIZTi8MS8Cw+O/Qhym1271ThMlhD6PubSYJXfE6rVbE7A9RzH73A6MmKBlzK8VTb4SlNSrr/DOk+L0uq+wPkv+pm+D9WtxoqQ9yl6FaK1cPawa3+7yRNle3m+72KCtyMkQv"),
            r#"
                -----BEGIN RSA PRIVATE KEY-----
                Proc-Type: 4,ENCRYPTED
                DEK-Info: AES-128-CBC,818C7722D3B01F2161C2ACF6A5BBAAE8
                
                3Cht4QB3PcoQ0I55j1B3m2ZzIC/mrh+K5nQeA1Vy2GBTMyM7yqGHqTOv7qLhJscd
                H+cB0Pm6yCr3lYuNrcKWOCUto+91P7ikyARruHVwyIxKdNx15uNulOzQJHQWNbA4
                RQHlhjON4atVo2FyJ6n+ujK6QiBg2PR5Vbbw/AtV6zBCFW3PhzDn+qqmHjpBFqj2
                vZUUe+MkDQcaF5J45XMHahhSdo/uKCDhfbylExp/+ACWkvxdPpsvcARM6X434ucD
                aPY+4i0/JyLkdbm0GFN9/q3i53qf4kCBhojFl4AYJdGI0AzAgbdTXZ7EJHbAGZHS
                os5K0oTwDVXMI0sSE2I/qHxaZZsDP1dOKq6di6SFPUp8liYimm7rNintRX88Gl2L
                g1ko9abp/NlgD0YY/3mad+NNAISDL/YfXq2fklH3En3/7ZrOVZFKfZXwQwas5g+p
                VQPKi3+ae74iOjLyuPDSc1ePmhUNYeP+9rLSc0wiaiHqls+2blPPDxAGMEo63kbz
                YPVjdmuVX4VWnyEsfTxxJdFDYGSNh6rlrrO1RFrex7kJvpg5gTX4M/FT8TfCd7Hn
                M6adXsLMqwu5tz8FuDmAtVdq8zdSrgZeAbpJ9D3EDOmZ70xz4XBL19ImxDp+Qqs2
                kQX7kobRzeeP2URfRoGr7XZikQWyQ2UASfPcQULY8R58QoZWWsQ4w51GZHg7TDnw
                1DRo/0OgkK7Gqf215nFmMpB4uyi58cq3WFwWQa1IqslkObpVgBQZcNZb/hKUYPGk
                g4zehfIgAfCdnQHwZvQ6Fdzhcs3SZeO+zVyuiZN3Gsi9HU0/1vpAKiuuOzcG02vF
                b6Y6hwsAA9yphF3atI+ARD4ZwXdDfzuGb3yJglMT3Fr/xuLwAvdchRo1spANKA0E
                tT5okLrK0H4wnHvf2SniVVWRhmJis0lQo9LjGGwRIdsPpVnJSDvaISIVF+fHT90r
                HvxN8zXI93x9jcPtwp7puQ1C7ehKJK10sZ71OLIZeuUgwt+5DRunqg6evPco9Go7
                UOGwcVhLY200KT+1k7zWzCS0yVQp2HRm6cxsZXAp4ClBSwIx15eIoLIrjZdJRjCq
                COp6pZx1fnvJ9ERIvl5hon+Ty+renMcFKz2HmchC7egpcqIxW9Dsv6zjhHle6pxb
                37GaEKHF2KA3RN+dSV/K8n+C9Yent5tx5Y9a/pMcgRGtgu+G+nyFmkPKn5Zt39yX
                qDpyM0LtbRVZPs+MgiqoGIwYc/ujoCq7GL38gezsBQoHaTt79yYBqCp6UR0LMuZ5
                f/7CtWqffgySfJ/0wjGidDAumDv8CK45AURpL/Z+tbFG3M9ar/LZz/Y6EyBcLtGY
                Wwb4zs8zXIA0qHrjNTnPqHDvezziArYfgPjxCIHMZzms9Yn8+N02p39uIytqg434
                BAlCqZ7GYdDFfTpWIwX+segTK9ux0KdBqcQv+9Fwwjkq9KySnRKqNl7ZJcefFZJq
                c6PA1iinZWBjuaO1HKx3PFulrl0bcpR9Kud1ZIyfnh5rwYN8UQkkcR/wZPla04TY
                8l5dq/LI/3G5sZXwUHKOcuQWTj7Saq7Q6gkKoMfqt0wC5bpZ1m17GHPoMz6GtX9O
                -----END RSA PRIVATE KEY-----
            "#,
            Some("test123"));
        assert!(cred.is_ok());
    }
    

    #[cfg(unix)]
    fn chmod(path: &Path) {
        use std::os::unix::prelude::*;
        use std::fs;
        let mut perms = fs::metadata(path).unwrap().permissions();
        perms.set_mode(0o755);
        fs::set_permissions(path, perms).unwrap();
    }
    #[cfg(windows)]
    fn chmod(_path: &Path) {}
}
