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

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

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_clone(url: &str, sha1: &str) -> Result<TempDir, Error> {
    let out = TempDir::new()?;

    let path = out.path().to_string_lossy();
    let cmd = ["clone", url, path.as_ref(), "--revision", sha1, "--depth", "1"];

    debug!("Cloning git repository: {}", url);
    trace!("Cloning git repository into: {}", path);

    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).to_string();
        let stderr = String::from_utf8_lossy(&result.stderr).to_string();

        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 {
            cmd: cmd.join(" "),
            stdout,
            stderr,
        });
    }

    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))
    }
}
