use std::io;
use std::path::{Path, PathBuf};

use log::{debug, trace, warn};
use tempfile::TempDir;
use tokio::process::Command;

use crate::error::Error;
use crate::metadata::{Metadata, get_crate_metadata_from_workspace};
use crate::utils::{self, DirectoryContents, get_contents};
use crate::workarounds::find_in_vcs;

pub(crate) async fn git_version() -> Result<(u8, u8), Error> {
    debug!("Looking for the 'git' executable and checking its version");

    trace!("Running command: git --version");
    let result = Command::new("git").arg("--version").output().await?;

    let stdout = String::from_utf8_lossy(&result.stdout);
    let stderr = String::from_utf8_lossy(&result.stderr);

    if !result.status.success() {
        return Err(Error::subprocess(
            "git",
            &["--version"],
            stdout.to_string(),
            stderr.to_string(),
        ));
    }

    match parse_git_version(&stdout) {
        Some(v) => Ok(v),
        None => Err(Error::subprocess(
            "git",
            &["--version"],
            stdout.to_string(),
            stderr.to_string(),
        )),
    }
}

fn parse_git_version(string: &str) -> Option<(u8, u8)> {
    let string_split = string.split(' ').collect::<Vec<&str>>();
    let Ok(["git", "version", version_str]) = TryInto::<[&str; 3]>::try_into(string_split) else {
        return None;
    };

    let version_split = version_str.split('.').collect::<Vec<&str>>();
    let Ok([maj_str, min_str, _mic_str]) = TryInto::<[&str; 3]>::try_into(version_split) else {
        return None;
    };

    Some((maj_str.parse().ok()?, min_str.parse().ok()?))
}

pub(crate) async fn git_clone(url: &str, sha1: &str) -> Result<TempDir, Error> {
    let (maj, min) = git_version().await?;
    if maj >= 2 && min >= 49 {
        trace!("git v2.49.0 or newer found, using shallow clone.");
        git_clone_shallow(url, sha1).await
    } else {
        warn!("git v2.49.0 or newer is not available, using fallback code for non-shallow clones.");
        git_clone_fallback(url, sha1).await
    }
}

async fn git_clone_shallow(url: &str, sha1: &str) -> Result<TempDir, Error> {
    debug!("Cloning git repository: {url}");

    let out = TempDir::new()?;
    let path = out.path().to_string_lossy();
    trace!("Cloning git repository into: {path}");

    let cmd = ["clone", url, path.as_ref(), "--revision", sha1, "--depth", "1"];
    trace!("Running command: git {}", cmd.join(" "));

    let result = Command::new("git")
        .args(cmd.iter())
        .env("GIT_TERMINAL_PROMPT", "0")
        .output()
        .await?;

    if !result.status.success() {
        let stdout = String::from_utf8_lossy(&result.stdout);
        let stderr = String::from_utf8_lossy(&result.stderr);

        if stderr.contains("terminal prompts disabled") {
            // specified URL points to host that supports git,
            // but the specified repository does not exist?
            return Err(Error::InvalidRepoUrl {
                repo: String::from(url),
            });
        }

        if stderr.contains("dumb http transport") {
            // specified URL not actually pointing at a git repository?
            return Err(Error::InvalidRepoUrl {
                repo: String::from(url),
            });
        }

        if stderr.contains(&format!("not our ref {sha1}")) {
            // specified git ref not found in the remote?
            return Err(Error::InvalidGitRef {
                repo: String::from(url),
                rev: String::from(sha1),
            });
        }

        // other error
        return Err(Error::subprocess("git", &cmd, stdout.to_string(), stderr.to_string()));
    }

    Ok(out)
}

async fn git_clone_fallback(url: &str, sha1: &str) -> Result<TempDir, Error> {
    debug!("Cloning git repository: {url}");

    let out = TempDir::new()?;
    let path = out.path().to_string_lossy();
    trace!("Cloning git repository into: {path}");

    let cmd = ["clone", url, path.as_ref()];
    trace!("Running command: git {}", cmd.join(" "));

    let result = Command::new("git")
        .args(cmd.iter())
        .env("GIT_TERMINAL_PROMPT", "0")
        .output()
        .await?;

    if !result.status.success() {
        let stdout = String::from_utf8_lossy(&result.stdout);
        let stderr = String::from_utf8_lossy(&result.stderr);

        if stderr.contains("terminal prompts disabled") {
            // specified URL points to host that supports git,
            // but the specified repository does not exist?
            return Err(Error::InvalidRepoUrl {
                repo: String::from(url),
            });
        }

        if stderr.contains("dumb http transport") {
            // specified URL not actually pointing at a git repository?
            return Err(Error::InvalidRepoUrl {
                repo: String::from(url),
            });
        }

        // other error
        return Err(Error::subprocess("git", &cmd, stdout.to_string(), stderr.to_string()));
    }

    let cmd = ["-C", path.as_ref(), "checkout", "--detach", sha1];
    trace!("Running command: git {}", cmd.join(" "));

    let result = Command::new("git")
        .args(cmd.iter())
        .env("GIT_TERMINAL_PROMPT", "0")
        .output()
        .await?;

    if !result.status.success() {
        let stdout = String::from_utf8_lossy(&result.stdout);
        let stderr = String::from_utf8_lossy(&result.stderr);

        if stderr.contains(&format!("unable to read tree {sha1}")) {
            // specified git ref not found in the repository?
            return Err(Error::InvalidGitRef {
                repo: String::from(url),
                rev: String::from(sha1),
            });
        }

        if stderr.contains("--detach does not take a path argument") {
            // specified git ref not found in the repository?
            return Err(Error::InvalidGitRef {
                repo: String::from(url),
                rev: String::from(sha1),
            });
        }

        // other error
        return Err(Error::subprocess("git", &cmd, stdout.to_string(), stderr.to_string()));
    }

    Ok(out)
}

#[derive(Debug)]
#[non_exhaustive]
pub struct Repository<'a> {
    pub metadata: Metadata,
    pub root: PathBuf,
    pub id: &'a str,
    pub path_in_vcs: String,
    _temp: Option<TempDir>,
}

impl<'a> Repository<'a> {
    pub(crate) async fn clone(url: &str, id: &'a str, path_in_vcs: Option<&str>, name: &str) -> Result<Self, Error> {
        let temp = git_clone(url, id).await?;

        let (path_in_vcs, metadata) = if let Some(path_in_vcs) = path_in_vcs {
            // path_in_vcs determined from '.cargo_vcs_info.json'
            let metadata = get_crate_metadata_from_workspace(temp.as_ref(), path_in_vcs, name)?;
            (path_in_vcs.to_string(), metadata)
        } else {
            debug!("Using heuristics to find crate in repository");
            let Some((detected_path_in_vcs, metadata)) = find_in_vcs(&temp, name) else {
                return Err(Error::PathNotDeterminable {
                    repo: url.to_string(),
                    name: name.to_string(),
                });
            };
            debug!("Crate found at: '{detected_path_in_vcs}'");
            (detected_path_in_vcs, metadata)
        };

        Ok(Repository {
            metadata,
            root: temp.path().to_owned(),
            id,
            path_in_vcs,
            _temp: Some(temp),
        })
    }

    pub(crate) fn cargo_toml(&self) -> PathBuf {
        self.root.join(&self.path_in_vcs).join("Cargo.toml")
    }

    pub(crate) fn file_contents(&self) -> Result<DirectoryContents, Error> {
        let mut contents = get_contents(&self.root.join(&self.path_in_vcs), &self.root)?;
        contents
            .files
            .retain(|f| f.file_name().is_some_and(|s| s != "Cargo.toml"));
        Ok(contents)
    }

    pub(crate) fn read_entry_to_bytes<P: AsRef<Path>>(&self, path: P) -> io::Result<Vec<u8>> {
        trace!("Reading bytes from file: {}", path.as_ref().to_string_lossy());
        utils::read_to_bytes(self.root.join(&self.path_in_vcs).join(path))
    }

    pub(crate) fn read_entry_to_string<P: AsRef<Path>>(&self, path: P) -> io::Result<String> {
        trace!("Reading text from file: {}", path.as_ref().to_string_lossy());
        std::fs::read_to_string(self.root.join(&self.path_in_vcs).join(path))
    }
}

#[cfg(test)]
mod tests {
    use super::parse_git_version;

    #[test]
    fn test_parse_git_version() {
        assert_eq!(parse_git_version("git version 2.49.0"), Some((2u8, 49u8)));
        assert_eq!(parse_git_version("git version 2.55.0"), Some((2u8, 55u8)));
        assert_eq!(parse_git_version("foobar"), None);
    }
}
