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

use anyhow::{Result, anyhow};
use console::style;
use tokio::process::Command;

fn is_auth_error(stderr: &str) -> bool {
    let auth_patterns = [
        "authentication failed",
        "permission denied",
        "could not resolve host",
        "repository not found",
        "could not read username",
        "could not read password",
        "could not read from remote",
        "fatal: the remote end hung up",
        "terminal prompts disabled",
        "passphrase",
        "enter passphrase for key",
        "permission denied (publickey)",
        "permission denied (password)",
    ];

    let lower = stderr.to_lowercase();
    auth_patterns.iter().any(|pattern| lower.contains(pattern))
}

fn set_noninteractive_env(command: &mut Command) {
    // Disable interactive prompts to prevent git from hanging on authentication
    command.env("GIT_TERMINAL_PROMPT", "0");
    command.env("GIT_ASKPASS", "");
    command.env("SSH_ASKPASS", "");
    // Force SSH to use batch mode - fails immediately instead of prompting
    command.env("GIT_SSH_COMMAND", "ssh -o BatchMode=yes");
}

async fn run_command(command: &mut Command, label: Display<'_>) -> Result<()> {
    set_noninteractive_env(command);
    let output = command.output().await?;
    let stderr = String::from_utf8_lossy(&output.stderr);

    if !output.status.success() {
        if is_auth_error(&stderr) {
            anyhow::bail!(
                "Authentication required. Configure credentials using:\n\
                 - git credential manager\n\
                 - SSH keys (without passphrase or via ssh-agent)\n\
                 - Personal access token\n\
                 Error: {}",
                stderr
            );
        }
        anyhow::bail!(
            "Command failed with exit code {}: {}",
            output.status,
            stderr
        );
    }

    println!("{}", style(label).cyan().bold());
    let stdout = String::from_utf8_lossy(&output.stdout);
    if !stdout.is_empty() {
        println!("{}", stdout);
    }

    Ok(())
}

async fn run_git_command(repo_path: &Path, args: &[&str], label: Display<'_>) -> Result<()> {
    let mut command = Command::new("git");
    command.args(args).current_dir(repo_path);
    run_command(&mut command, label).await
}

pub async fn checkout_default(repo_path: &Path) -> Result<()> {
    if run_git_command(repo_path, &["checkout", "main"], repo_path.display())
        .await
        .is_ok()
    {
        return Ok(());
    }

    if run_git_command(repo_path, &["checkout", "master"], repo_path.display())
        .await
        .is_ok()
    {
        return Ok(());
    }

    Err(anyhow!(
        "Neither 'main' nor 'master' exists in this repository"
    ))
}

// Custom status command with color-coded headers based on repository state
pub async fn status(repo_path: &Path, dirty_only: bool) -> Result<()> {
    let mut command = Command::new("git");
    command.arg("status").current_dir(repo_path);
    set_noninteractive_env(&mut command);

    let output = command.output().await?;
    let stderr = String::from_utf8_lossy(&output.stderr);

    if !output.status.success() {
        if is_auth_error(&stderr) {
            anyhow::bail!(
                "Authentication required. Configure credentials using:\n\
                 - git credential manager\n\
                 - SSH keys (without passphrase or via ssh-agent)\n\
                 - Personal access token\n\
                 Error: {}",
                stderr
            );
        }
        anyhow::bail!(
            "Command failed with exit code {}: {}",
            output.status,
            stderr
        );
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let status_output = stdout.to_string();

    // Analyze status to determine state
    let status_lower = status_output.to_lowercase();

    // Check if repo needs sync with remote (red)
    let needs_sync = status_lower.contains("behind")
        || status_lower.contains("ahead")
        || status_lower.contains("diverged")
        || status_lower.contains("conflict")
        || status_lower.contains("unmerged");

    // Check for uncommitted changes (yellow)
    let has_changes = status_lower.contains("changes to be committed")
        || status_lower.contains("changes not staged")
        || status_lower.contains("untracked files");

    if dirty_only && !needs_sync && !has_changes {
        return Ok(());
    }

    // Print header with appropriate color
    let repo_name = repo_path.display();
    if needs_sync {
        println!("{}", style(repo_name).red().bold());
    } else if has_changes {
        println!("{}", style(repo_name).yellow().bold());
    } else {
        println!("{}", style(repo_name).cyan().bold());
    }

    // Print status output
    if !status_output.is_empty() {
        println!("{}", status_output);
    }

    Ok(())
}

pub async fn clone(target_dir: PathBuf, repo: String) -> Result<()> {
    let mut command = Command::new("git");
    command.arg("clone").arg(&repo).current_dir(&target_dir);
    let repo_name = repo
        .rsplit('/')
        .next()
        .unwrap_or(&repo)
        .trim_end_matches(".git");
    let target_path = target_dir.join(repo_name);
    let r = run_command(&mut command, target_path.display()).await;
    println!("{}", style(format!("✓ Cloned repo: {}", repo)).green());
    r
}

pub async fn exec(repo_path: &Path, command_args: &[String]) -> Result<()> {
    if command_args.is_empty() {
        anyhow::bail!("No command provided for exec");
    }
    let mut command = Command::new(&command_args[0]);
    command.args(&command_args[1..]).current_dir(repo_path);
    run_command(&mut command, repo_path.display()).await
}

#[cfg(test)]
mod tests {
    use std::path::Path;
    use std::process::Command as StdCommand;

    use tempfile::tempdir;

    use super::{checkout_default, clone, exec, is_auth_error, run_command, status};

    fn run_git(dir: &Path, args: &[&str]) {
        let output = StdCommand::new("git")
            .args(args)
            .current_dir(dir)
            .output()
            .unwrap();
        assert!(output.status.success());
    }

    fn init_repo(dir: &Path) {
        init_repo_with_branch(dir, "main");
    }

    fn init_repo_with_branch(dir: &Path, branch: &str) {
        run_git(dir, &["init", "-b", branch]);
        std::fs::write(dir.join("README.md"), "madgit\n").unwrap();
        run_git(dir, &["add", "README.md"]);
        run_git(
            dir,
            &[
                "-c",
                "user.name=Test User",
                "-c",
                "user.email=test@example.com",
                "commit",
                "-m",
                "initial",
            ],
        );
    }

    async fn shell_error(dir: &Path, script: &str) -> anyhow::Error {
        let mut command = tokio::process::Command::new("sh");
        command.arg("-c").arg(script).current_dir(dir);
        run_command(&mut command, dir.display()).await.unwrap_err()
    }

    #[test]
    fn is_auth_error_matches_known_authentication_messages() {
        assert!(is_auth_error(
            "fatal: Authentication failed for 'https://example.com/repo.git'"
        ));
        assert!(is_auth_error("Permission denied (publickey)."));
        assert!(is_auth_error(
            "Enter passphrase for key '/home/user/.ssh/id_ed25519':"
        ));
    }

    #[test]
    fn is_auth_error_ignores_unrelated_errors() {
        assert!(!is_auth_error(
            "fatal: not a git repository (or any of the parent directories): .git"
        ));
    }

    #[tokio::test]
    async fn exec_errors_when_no_command_is_provided() {
        let temp_dir = tempdir().unwrap();
        let error = exec(temp_dir.path(), &[]).await.unwrap_err();

        assert_eq!(error.to_string(), "No command provided for exec");
    }

    #[tokio::test]
    async fn status_succeeds_for_a_git_repository() {
        let temp_dir = tempdir().unwrap();
        init_repo(temp_dir.path());

        status(temp_dir.path(), false).await.unwrap();
    }

    #[tokio::test]
    async fn checkout_default_prefers_main_branch() {
        let temp_dir = tempdir().unwrap();
        init_repo_with_branch(temp_dir.path(), "main");

        checkout_default(temp_dir.path()).await.unwrap();

        let output = StdCommand::new("git")
            .args(["branch", "--show-current"])
            .current_dir(temp_dir.path())
            .output()
            .unwrap();
        assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "main");
    }

    #[tokio::test]
    async fn checkout_default_falls_back_to_master_branch() {
        let temp_dir = tempdir().unwrap();
        init_repo_with_branch(temp_dir.path(), "master");

        checkout_default(temp_dir.path()).await.unwrap();

        let output = StdCommand::new("git")
            .args(["branch", "--show-current"])
            .current_dir(temp_dir.path())
            .output()
            .unwrap();
        assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "master");
    }

    #[tokio::test]
    async fn clone_clones_a_local_repository() {
        let source_dir = tempdir().unwrap();
        let target_dir = tempdir().unwrap();
        init_repo(source_dir.path());

        clone(
            target_dir.path().to_path_buf(),
            source_dir.path().to_string_lossy().to_string(),
        )
        .await
        .unwrap();

        assert!(
            target_dir
                .path()
                .join(source_dir.path().file_name().unwrap())
                .exists()
        );
    }

    #[tokio::test]
    async fn run_command_reports_authentication_failures() {
        let temp_dir = tempdir().unwrap();
        let error = shell_error(
            temp_dir.path(),
            "echo 'permission denied (publickey)' 1>&2; exit 1",
        )
        .await;

        assert!(error.to_string().contains("Authentication required"));
    }

    #[tokio::test]
    async fn run_command_reports_non_auth_failures() {
        let temp_dir = tempdir().unwrap();
        let error = shell_error(temp_dir.path(), "echo 'boom' 1>&2; exit 2").await;

        assert!(error.to_string().contains("Command failed with exit code"));
    }
}
