use std::collections::HashMap;
use std::path::{Path, PathBuf};

use anyhow::Result;
use console::style;
use futures::future::join_all;
use tokio::fs;
use tokio::task::JoinSet;

use crate::git;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum ErrorCategory {
    Authentication,
    Network,
    Repository,
    NoRemote,
    Other,
}

impl ErrorCategory {
    fn as_str(&self) -> &'static str {
        match self {
            ErrorCategory::Authentication => "Authentication",
            ErrorCategory::Network => "Network",
            ErrorCategory::Repository => "Repository",
            ErrorCategory::NoRemote => "NoRemote",
            ErrorCategory::Other => "Other",
        }
    }

    fn icon(&self) -> &'static str {
        match self {
            ErrorCategory::Authentication => "🔐",
            ErrorCategory::Network => "🌐",
            ErrorCategory::Repository => "📁",
            ErrorCategory::NoRemote => "💻",
            ErrorCategory::Other => "⚠️",
        }
    }

    fn all() -> [ErrorCategory; 5] {
        [
            ErrorCategory::Authentication,
            ErrorCategory::Network,
            ErrorCategory::Repository,
            ErrorCategory::NoRemote,
            ErrorCategory::Other,
        ]
    }
}

fn categorize_error(error: &str) -> ErrorCategory {
    let lower = error.to_lowercase();
    if lower.contains("authentication")
        || lower.contains("permission denied")
        || lower.contains("passphrase")
        || lower.contains("publickey")
        || lower.contains("could not read")
    {
        ErrorCategory::Authentication
    } else if lower.contains("could not resolve")
        || lower.contains("network")
        || lower.contains("connection")
        || lower.contains("timeout")
    {
        ErrorCategory::Network
    } else if lower.contains("not a git repository") || lower.contains("does not exist") {
        ErrorCategory::Repository
    } else if lower.contains("no tracking information")
        || lower.contains("no upstream branch")
        || lower.contains("no remote")
        || lower.contains("does not have an upstream")
    {
        ErrorCategory::NoRemote
    } else {
        ErrorCategory::Other
    }
}

fn print_error_summary(success_count: usize, errors: &[(String, String)]) {
    if errors.is_empty() {
        println!(
            "\n{}",
            style("✓ All operations completed successfully")
                .green()
                .bold()
        );
        return;
    }

    let total = success_count + errors.len();

    // Group errors by category
    let mut by_category: HashMap<ErrorCategory, Vec<(String, String)>> = HashMap::new();
    for (repo, error) in errors {
        let category = categorize_error(error);
        by_category
            .entry(category)
            .or_default()
            .push((repo.clone(), error.clone()));
    }

    // Print verbose errors by category first
    println!("\n{}", style("═".repeat(60)).dim());
    println!("{}", style("ERROR REPORT").bold().underlined());
    println!("{}", style("═".repeat(60)).dim());

    for category in &ErrorCategory::all() {
        if let Some(errors_in_category) = by_category.get(category) {
            println!(
                "\n{} {} ({} errors)",
                category.icon(),
                style(format!("[{}]", category.as_str())).bold(),
                style(errors_in_category.len().to_string()).yellow()
            );

            // Print full errors for each repository
            for (repo, error) in errors_in_category.iter() {
                println!("\n  {}", style(format!("📁 {}", repo)).cyan().bold());

                // Print full error message with proper indentation
                for line in error.lines() {
                    println!("     {}", style(line).dim());
                }
            }
        }
    }

    // Print short summary at the end
    println!("\n{}", style("─".repeat(60)).dim());
    println!("{}", style("SUMMARY").bold());
    println!("{}", style("─".repeat(60)).dim());
    println!(
        "{:<20} {}/{}",
        style("Successful:").green(),
        style(success_count.to_string()).green().bold(),
        total
    );
    println!(
        "{:<20} {}/{}",
        style("Failed:").red(),
        style(errors.len().to_string()).red().bold(),
        total
    );
    println!();
}

async fn for_each_repo<F, Fut>(dir_path: &Path, op: F) -> Result<()>
where
    F: Fn(PathBuf) -> Fut + Clone,
    Fut: std::future::Future<Output = Result<()>>,
{
    let mut entries = fs::read_dir(dir_path).await?;
    let mut repos = Vec::new();

    while let Some(entry) = entries.next_entry().await? {
        let path = entry.path();
        if path.is_dir() && path.join(".git").exists() {
            repos.push(path);
        }
    }

    if repos.is_empty() {
        return Ok(());
    }

    // Process all repos concurrently
    let tasks: Vec<_> = repos
        .into_iter()
        .map(|repo_path| {
            let op = op.clone();
            async move {
                let result = op(repo_path.clone()).await;
                (repo_path, result)
            }
        })
        .collect();

    let results = join_all(tasks).await;
    let mut success_count = 0;
    let mut errors: Vec<(String, String)> = Vec::new();

    for (repo_path, result) in results {
        let repo_name = repo_path
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("unknown")
            .to_string();

        match result {
            Ok(()) => success_count += 1,
            Err(e) => {
                errors.push((repo_name, e.to_string()));
            }
        }
    }

    print_error_summary(success_count, &errors);

    if !errors.is_empty() {
        anyhow::bail!("Some operations failed");
    }

    Ok(())
}

macro_rules! mass_command {
    ($name:ident) => {
        pub async fn $name(dir_path: &Path) -> Result<()> {
            for_each_repo(
                dir_path,
                |repo_path| async move { git::$name(&repo_path).await },
            )
            .await
        }
    };
    ($name:ident, $param_type:ty, $param_name:ident) => {
        pub async fn $name(dir_path: &Path, $param_name: $param_type) -> Result<()> {
            for_each_repo(dir_path, |repo_path| async move {
                git::$name(&repo_path, $param_name).await
            })
            .await
        }
    };
}

mass_command!(status, bool, dirty);
mass_command!(checkout_default);

pub async fn exec(dir_path: &Path, command_args: &[String]) -> Result<()> {
    for_each_repo(dir_path, |repo_path| async move {
        git::exec(&repo_path, command_args).await
    })
    .await
}

pub async fn clone(target_dir: &Path, repos: Vec<String>) -> Result<()> {
    let target_dir = target_dir.to_path_buf();

    let mut tasks = JoinSet::new();

    for repo in repos {
        let target_dir = target_dir.clone();
        tasks.spawn(async move {
            let result = git::clone(target_dir, repo.clone()).await;
            (repo, result)
        });
    }

    let mut success_count = 0;
    let mut errors: Vec<(String, String)> = Vec::new();

    while let Some(result) = tasks.join_next().await {
        match result {
            Ok((_, Ok(()))) => {
                success_count += 1;
            }
            Ok((repo, Err(e))) => {
                errors.push((repo, e.to_string()));
            }
            Err(e) if e.is_panic() => {
                errors.push(("<unknown>".to_string(), format!("Task panicked: {}", e)));
            }
            Err(e) => {
                errors.push(("<unknown>".to_string(), format!("Task failed: {}", e)));
            }
        }
    }

    print_error_summary(success_count, &errors);

    if !errors.is_empty() {
        anyhow::bail!("Some clone operations failed");
    }

    Ok(())
}

pub async fn nogit(dir_path: &Path) -> Result<()> {
    let mut entries = fs::read_dir(dir_path).await?;
    let mut found = false;

    while let Some(entry) = entries.next_entry().await? {
        let path = entry.path();
        if path.is_dir() && !path.join(".git").exists() {
            println!("{}", style(path.display()).yellow());
            found = true;
        }
    }

    if !found {
        println!(
            "{}",
            style("✓ All subdirectories are git repositories").green()
        );
    } else {
        println!(
            "\n{}",
            style("Tip: Use 'madgit status' to check repository status").dim()
        );
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use tempfile::tempdir;

    use super::{ErrorCategory, categorize_error, for_each_repo};

    #[test]
    fn categorize_error_detects_authentication_failures() {
        assert_eq!(
            categorize_error("Permission denied (publickey)"),
            ErrorCategory::Authentication
        );
    }

    #[test]
    fn categorize_error_detects_network_failures() {
        assert_eq!(
            categorize_error("fatal: Could not resolve host: gitlab.com"),
            ErrorCategory::Network
        );
    }

    #[test]
    fn categorize_error_detects_missing_upstream_failures() {
        assert_eq!(
            categorize_error("There is no tracking information for the current branch."),
            ErrorCategory::NoRemote
        );
    }

    #[test]
    fn categorize_error_falls_back_to_other_for_unknown_failures() {
        assert_eq!(
            categorize_error("unexpected git failure"),
            ErrorCategory::Other
        );
    }

    #[test]
    fn categorize_error_detects_repository_failures() {
        assert_eq!(
            categorize_error("fatal: not a git repository"),
            ErrorCategory::Repository
        );
    }

    #[test]
    fn error_category_helpers_are_stable() {
        assert_eq!(ErrorCategory::Authentication.as_str(), "Authentication");
        assert_eq!(ErrorCategory::Network.icon(), "🌐");
        assert_eq!(ErrorCategory::Repository.as_str(), "Repository");
        assert_eq!(ErrorCategory::NoRemote.as_str(), "NoRemote");
        assert_eq!(ErrorCategory::Other.as_str(), "Other");
        assert_eq!(ErrorCategory::Authentication.icon(), "🔐");
        assert_eq!(ErrorCategory::Repository.icon(), "📁");
        assert_eq!(ErrorCategory::NoRemote.icon(), "💻");
        assert_eq!(ErrorCategory::Other.icon(), "⚠️");
        assert_eq!(ErrorCategory::all().len(), 5);
    }

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

        for_each_repo(temp_dir.path(), |_repo_path| async move { Ok(()) })
            .await
            .unwrap();
    }

    #[tokio::test]
    async fn for_each_repo_ignores_directories_without_git_metadata() {
        let temp_dir = tempdir().unwrap();
        std::fs::create_dir(temp_dir.path().join("repo-a")).unwrap();
        std::fs::create_dir(temp_dir.path().join("repo-a/.git")).unwrap();
        std::fs::create_dir(temp_dir.path().join("not-a-repo")).unwrap();

        let visited = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
        let visited_for_closure = visited.clone();

        for_each_repo(temp_dir.path(), move |repo_path| {
            let visited = visited_for_closure.clone();
            async move {
                visited
                    .lock()
                    .unwrap()
                    .push(repo_path.file_name().unwrap().to_string_lossy().to_string());
                Ok(())
            }
        })
        .await
        .unwrap();

        assert_eq!(visited.lock().unwrap().as_slice(), ["repo-a"]);
    }

    #[tokio::test]
    async fn for_each_repo_returns_error_when_any_operation_fails() {
        let temp_dir = tempdir().unwrap();
        for name in ["repo-a", "repo-b"] {
            std::fs::create_dir(temp_dir.path().join(name)).unwrap();
            std::fs::create_dir(temp_dir.path().join(name).join(".git")).unwrap();
        }

        let result = for_each_repo(temp_dir.path(), |repo_path| async move {
            if repo_path.file_name().unwrap() == "repo-b" {
                anyhow::bail!("network timeout")
            }
            Ok(())
        })
        .await;

        assert!(result.is_err());
        assert_eq!(result.unwrap_err().to_string(), "Some operations failed");
    }

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

        let error = super::clone(
            temp_dir.path(),
            vec!["/definitely/missing/repo".to_string()],
        )
        .await
        .unwrap_err();

        assert_eq!(error.to_string(), "Some clone operations failed");
    }
}
