//! Git I/O — the ONLY place in `service::git_ops/` that may perform side effects.
//!
//! Side effects here include: spawning git processes via `std::process::Command`
//! (wrapped in `tokio::task::spawn_blocking`), and filesystem existence and
//! canonicalization checks for path validation.
//! All pure logic (path-prefix checks, porcelain parsing) lives in `path_guard.rs`
//! and `porcelain.rs`.

use super::Error;
use std::path::{Path, PathBuf};
use std::process::Output;

/// Validate that `worktree_path` is an absolute path that exists on disk and
/// lies inside the project's `.huskies/worktrees/` directory. Returns the
/// canonicalized path on success.
///
/// # Errors
/// - [`Error::Validation`] if the path is relative or does not exist.
/// - [`Error::PathNotAllowed`] if the path is outside `.huskies/worktrees/`.
/// - [`Error::Io`] if canonicalization fails.
pub fn validate_worktree_path(worktree_path: &str, project_root: &Path) -> Result<PathBuf, Error> {
    let wd = PathBuf::from(worktree_path);

    if !wd.is_absolute() {
        return Err(Error::Validation(
            "worktree_path must be an absolute path".to_string(),
        ));
    }
    if !wd.exists() {
        return Err(Error::Validation(format!(
            "worktree_path does not exist: {worktree_path}"
        )));
    }

    let worktrees_root = project_root.join(".huskies").join("worktrees");

    let canonical_wd = wd
        .canonicalize()
        .map_err(|e| Error::Io(format!("Cannot canonicalize worktree_path: {e}")))?;

    let canonical_wt = if worktrees_root.exists() {
        worktrees_root
            .canonicalize()
            .map_err(|e| Error::Io(format!("Cannot canonicalize worktrees root: {e}")))?
    } else {
        return Err(Error::PathNotAllowed(
            "No worktrees directory found in project".to_string(),
        ));
    };

    if !super::path_guard::is_under_root(&canonical_wd, &canonical_wt) {
        return Err(Error::PathNotAllowed(format!(
            "worktree_path must be inside .huskies/worktrees/. Got: {worktree_path}"
        )));
    }

    Ok(canonical_wd)
}

/// Run a git command with static arg slices in `dir` and return the process output.
///
/// # Errors
/// - [`Error::UpstreamFailure`] if the task panics or git cannot be spawned.
pub async fn run_git(args: Vec<&'static str>, dir: PathBuf) -> Result<Output, Error> {
    tokio::task::spawn_blocking(move || {
        std::process::Command::new("git")
            .args(&args)
            .current_dir(&dir)
            .output()
    })
    .await
    .map_err(|e| Error::UpstreamFailure(format!("Task join error: {e}")))?
    .map_err(|e| Error::Io(format!("Failed to run git: {e}")))
}

/// Run a git command with owned `String` args in `dir` and return the process output.
///
/// # Errors
/// - [`Error::UpstreamFailure`] if the task panics or git cannot be spawned.
pub async fn run_git_owned(args: Vec<String>, dir: PathBuf) -> Result<Output, Error> {
    tokio::task::spawn_blocking(move || {
        std::process::Command::new("git")
            .args(&args)
            .current_dir(&dir)
            .output()
    })
    .await
    .map_err(|e| Error::UpstreamFailure(format!("Task join error: {e}")))?
    .map_err(|e| Error::Io(format!("Failed to run git: {e}")))
}

/// Summary of uncommitted changes in a git working tree.
///
/// Returned by [`read_dirty_files_sync`] for use in status rendering.
#[derive(Debug, Default)]
pub struct DirtyFiles {
    /// Number of tracked files that have uncommitted changes (staged or unstaged).
    pub modified: usize,
    /// Number of untracked files.
    pub new: usize,
    /// All dirty file paths in the order `git status` reports them (deduplicated).
    pub paths: Vec<String>,
}

impl DirtyFiles {
    /// Returns `true` when the working tree has no uncommitted changes.
    pub fn is_clean(&self) -> bool {
        self.paths.is_empty()
    }
}

/// Run `git status --porcelain=v1 -u` synchronously in `dir` and return dirty file info.
///
/// Returns a zeroed [`DirtyFiles`] if `dir` is not a git repo, git is unavailable, or the
/// working tree is clean.
pub fn read_dirty_files_sync(dir: &Path) -> DirtyFiles {
    let output = match std::process::Command::new("git")
        .args(["status", "--porcelain=v1", "-u"])
        .current_dir(dir)
        .output()
    {
        Ok(o) => o,
        Err(_) => return DirtyFiles::default(),
    };
    if !output.status.success() && output.stdout.is_empty() {
        return DirtyFiles::default();
    }
    let stdout = String::from_utf8_lossy(&output.stdout);
    let (staged, unstaged, untracked) = super::porcelain::parse_git_status_porcelain(&stdout);
    let mut seen = std::collections::HashSet::new();
    let mut paths = Vec::new();
    let mut modified_set = std::collections::HashSet::new();
    for path in staged.iter().chain(unstaged.iter()) {
        modified_set.insert(path.clone());
        if seen.insert(path.clone()) {
            paths.push(path.clone());
        }
    }
    for path in &untracked {
        if seen.insert(path.clone()) {
            paths.push(path.clone());
        }
    }
    DirtyFiles {
        modified: modified_set.len(),
        new: untracked.len(),
        paths,
    }
}
