use std::sync::Arc;

#[cfg(not(target_arch = "wasm32"))]
use std::{
    path::{Path, PathBuf},
    process::Command,
};

#[cfg(not(target_arch = "wasm32"))]
use minijinja::value::from_args;
use minijinja::{Value, value::Object};

#[derive(Debug)]
pub(crate) struct Git {
    pub(crate) experimental: bool,
    #[cfg(not(target_arch = "wasm32"))]
    pub(crate) recipe_dir: Option<PathBuf>,
}

impl std::fmt::Display for Git {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("Git")
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn get_command_output(command: &str, args: &[&str]) -> Result<String, minijinja::Error> {
    let output = Command::new(command).args(args).output().map_err(|e| {
        minijinja::Error::new(minijinja::ErrorKind::InvalidOperation, e.to_string())
    })?;

    if !output.status.success() {
        Err(minijinja::Error::new(
            minijinja::ErrorKind::InvalidOperation,
            String::from_utf8_lossy(&output.stderr).to_string(),
        ))
    } else {
        Ok(String::from_utf8(output.stdout).map_err(|e| {
            minijinja::Error::new(minijinja::ErrorKind::InvalidOperation, e.to_string())
        })?)
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn git_command_in_dir(dir: &Path, args: &[&str]) -> Result<String, minijinja::Error> {
    let output = Command::new("git")
        .current_dir(dir)
        .args(args)
        .output()
        .map_err(|e| {
            minijinja::Error::new(minijinja::ErrorKind::InvalidOperation, e.to_string())
        })?;

    if !output.status.success() {
        Err(minijinja::Error::new(
            minijinja::ErrorKind::InvalidOperation,
            String::from_utf8_lossy(&output.stderr).to_string(),
        ))
    } else {
        Ok(String::from_utf8(output.stdout).map_err(|e| {
            minijinja::Error::new(minijinja::ErrorKind::InvalidOperation, e.to_string())
        })?)
    }
}

#[cfg(not(target_arch = "wasm32"))]
impl Git {
    /// Try to resolve `src` as a local path. Relative paths are resolved
    /// against the recipe directory only. Absolute paths are checked directly.
    /// Returns `None` if `src` does not point to a local directory (i.e. it is
    /// likely a remote URL).
    fn resolve_local_path(&self, src: &str) -> Option<PathBuf> {
        let path = Path::new(src);

        // Absolute paths are checked directly
        if path.is_absolute() {
            return if path.is_dir() {
                Some(path.to_path_buf())
            } else {
                None
            };
        }

        // Relative paths are resolved against the recipe directory
        if let Some(recipe_dir) = &self.recipe_dir {
            let resolved = recipe_dir.join(src);
            if resolved.is_dir() {
                return Some(resolved);
            }
        }

        None
    }

    fn head_rev(&self, src: &str) -> Result<Value, minijinja::Error> {
        if let Some(local_path) = self.resolve_local_path(src) {
            let result = git_command_in_dir(&local_path, &["rev-parse", "HEAD"])?
                .trim()
                .to_string();
            return Ok(Value::from(result));
        }

        let result = get_command_output("git", &["ls-remote", src, "HEAD"])?
            .lines()
            .next()
            .and_then(|s| s.split_ascii_whitespace().nth(0))
            .ok_or_else(|| {
                minijinja::Error::new(
                    minijinja::ErrorKind::InvalidOperation,
                    "Failed to get the HEAD".to_string(),
                )
            })?
            .to_string();
        Ok(Value::from(result))
    }

    fn latest_tag_rev(&self, src: &str) -> Result<Value, minijinja::Error> {
        if let Some(local_path) = self.resolve_local_path(src) {
            let tag = git_command_in_dir(&local_path, &["describe", "--tags", "--abbrev=0"])?
                .trim()
                .to_string();
            let result = git_command_in_dir(&local_path, &["rev-list", "-n", "1", &tag])?
                .trim()
                .to_string();
            return Ok(Value::from(result));
        }

        let result = self.latest_tag_sha_remote(src)?;
        Ok(Value::from(result))
    }

    fn latest_tag_sha_remote(&self, src: &str) -> Result<String, minijinja::Error> {
        get_command_output("git", &["ls-remote", "--tags", "--sort=-v:refname", src])?
            .lines()
            .next()
            .and_then(|s| s.split_ascii_whitespace().nth(0))
            .ok_or_else(|| {
                minijinja::Error::new(
                    minijinja::ErrorKind::InvalidOperation,
                    "Failed to get the latest tag".to_string(),
                )
            })
            .map(|s| s.to_string())
    }

    fn latest_tag(&self, src: &str) -> Result<Value, minijinja::Error> {
        if let Some(local_path) = self.resolve_local_path(src) {
            let result = git_command_in_dir(&local_path, &["describe", "--tags", "--abbrev=0"])?
                .trim()
                .to_string();
            return Ok(Value::from(result));
        }

        let result = get_command_output("git", &["ls-remote", "--tags", "--sort=-v:refname", src])?
            .lines()
            .next()
            .and_then(|s| s.split_ascii_whitespace().nth(1))
            .and_then(|s| s.strip_prefix("refs/tags/"))
            .map(|s| s.trim_end_matches("^{}"))
            .ok_or_else(|| {
                minijinja::Error::new(
                    minijinja::ErrorKind::InvalidOperation,
                    "Failed to get the latest tag".to_string(),
                )
            })?
            .to_string();
        Ok(Value::from(result))
    }

    fn latest_tag_distance(&self, src: &str) -> Result<Value, minijinja::Error> {
        if let Some(local_path) = self.resolve_local_path(src) {
            let result = git_command_in_dir(&local_path, &["describe", "--tags", "--long"])?
                .trim()
                .rsplitn(3, '-')
                .collect::<Vec<&str>>()[1]
                .to_string();
            return Ok(Value::from(result));
        }

        let tag_rev = self.latest_tag_sha_remote(src)?;
        let distance = get_command_output(
            "git",
            &["rev-list", "--count", &format!("{}..HEAD", tag_rev)],
        )?
        .trim()
        .to_string();
        Ok(Value::from(distance))
    }

    fn dispatch(&self, name: &str, args: &[Value]) -> Result<Value, minijinja::Error> {
        let (src,) = from_args(args)?;
        match name {
            "head_rev" => self.head_rev(src),
            "latest_tag_rev" => self.latest_tag_rev(src),
            "latest_tag" => self.latest_tag(src),
            "latest_tag_distance" => self.latest_tag_distance(src),
            name => Err(minijinja::Error::new(
                minijinja::ErrorKind::UnknownMethod,
                format!("object has no method named {name}"),
            )),
        }
    }
}

#[cfg(target_arch = "wasm32")]
impl Git {
    fn dispatch(&self, _name: &str, _args: &[Value]) -> Result<Value, minijinja::Error> {
        Err(minijinja::Error::new(
            minijinja::ErrorKind::InvalidOperation,
            "Git operations are not available in WASM",
        ))
    }
}

impl Object for Git {
    fn call_method(
        self: &Arc<Self>,
        _state: &minijinja::State,
        name: &str,
        args: &[Value],
    ) -> Result<Value, minijinja::Error> {
        if !self.experimental {
            return Err(minijinja::Error::new(
                minijinja::ErrorKind::InvalidOperation,
                "Experimental feature: provide the `--experimental` flag to enable this feature",
            ));
        }

        self.dispatch(name, args)
    }
}
