use clap::{Parser, ValueEnum};
use indicatif::{ProgressBar, ProgressStyle};
use rayon::prelude::*;
use std::io::{self, IsTerminal, Write};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Duration, Instant};

#[derive(Parser, Debug)]
#[command(about = "Pulls the latest changes for every git repo in a directory")]
struct Cli {
    /// Directory whose immediate children should be scanned for git repositories
    #[arg(value_name = "ROOT", default_value = ".", value_hint = clap::ValueHint::DirPath)]
    root: PathBuf,
    /// Do not run any git commands; only show what would be executed
    #[arg(long)]
    dry_run: bool,
    /// Ask for confirmation before running git commands
    #[arg(long)]
    confirm: bool,
    /// Sort the summary table by repo name or duration
    #[arg(long, value_enum, default_value_t = SortBy::Name)]
    sort_by: SortBy,
}

struct Repo {
    name: String,
    path: PathBuf,
}

struct RepoResult {
    output: String,
    success: bool,
    exit_code: i32,
    duration: Duration,
}

#[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq)]
enum SortBy {
    Name,
    Duration,
}

fn main() -> anyhow::Result<()> {
    let run_started = Instant::now();
    let cli = Cli::parse();
    let root = canonicalize_root(cli.root)?;
    println!("Scanning repositories in {}", root.display());

    let repos = find_repositories(&root)?;
    if repos.is_empty() {
        println!("No git repositories found.");
        return Ok(());
    }

    if cli.confirm && !cli.dry_run {
        confirm_execution(repos.len())?;
    }

    let interactive = io::stdout().is_terminal();
    if interactive {
        println!("Progress: using interactive bar (TTY detected)");
    } else {
        println!("Progress: using plain lines (non-interactive output)");
    }

    let progress = build_progress_bar(repos.len() as u64);
    let mut processed: Vec<_> = repos
        .par_iter()
        .enumerate()
        .map_init(
            || {
                if interactive {
                    Some(progress.clone())
                } else {
                    None
                }
            },
            |pb, (idx, repo)| {
                let result = process_repo(repo, cli.dry_run);
                if let Some(pb) = pb.as_ref() {
                    if result.success {
                        pb.println(format!("[ ok ] {}", repo.name));
                    } else {
                        pb.println(format!("[err] {}", repo.name));
                    }
                    pb.inc(1);
                }
                (idx, result)
            },
        )
        .collect();

    if interactive {
        progress.finish_and_clear();
    }

    processed.sort_by_key(|(idx, _)| *idx);

    if !interactive {
        println!("\nRepository results (in order):");
        for (idx, result) in &processed {
            let repo = &repos[*idx];
            let tag = if result.success { "ok" } else { "err" };
            println!("[{tag}] {name}", name = repo.name);
        }
    }

    let mut error_count = 0usize;
    for (_, result) in &processed {
        println!("{}", result.output);
        if !result.success {
            error_count += 1;
        }
    }

    print_summary(&repos, &processed, cli.sort_by, run_started.elapsed());

    println!("\nAll repositories processed.");
    if error_count > 0 {
        println!("{} repositories reported errors.", error_count);
    }

    if error_count > 0 {
        std::process::exit(1);
    }

    Ok(())
}

fn build_progress_bar(len: u64) -> ProgressBar {
    let pb = ProgressBar::new(len);
    pb.set_style(
        ProgressStyle::with_template(
            "[{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} repos pulled",
        )
        .unwrap()
        .progress_chars("=>-"),
    );
    pb
}

fn canonicalize_root(root: PathBuf) -> anyhow::Result<PathBuf> {
    let mut path = root;
    if path.as_os_str().is_empty() {
        path = PathBuf::from(".");
    }
    Ok(path.canonicalize()?)
}

fn find_repositories(root: &Path) -> anyhow::Result<Vec<Repo>> {
    let mut repos = Vec::new();
    for entry in fs::read_dir(root)? {
        let entry = entry?;
        let path = entry.path();
        if !path.is_dir() {
            continue;
        }

        if path.join(".git").is_dir() {
            let name = entry.file_name().to_string_lossy().into_owned();
            repos.push(Repo { name, path });
        }
    }
    repos.sort_by(|a, b| a.name.cmp(&b.name));
    Ok(repos)
}

fn process_repo(repo: &Repo, dry_run: bool) -> RepoResult {
    let started = Instant::now();
    let mut output = String::new();
    output.push_str(&format!("\n===== {} =====\n", repo.name));

    if dry_run {
        output.push_str("[dry-run] would run: git status --short --branch\n");
        output.push_str("[dry-run] would run: git pull --ff-only\n");
        return RepoResult {
            output,
            success: true,
            exit_code: 0,
            duration: started.elapsed(),
        };
    }

    match run_git_command(&repo.path, &["status", "--short", "--branch"]) {
        Ok(result) => output.push_str(&result.body),
        Err(result) => {
            output.push_str(&result.body);
            return RepoResult {
                output,
                success: false,
                exit_code: result.exit_code.unwrap_or(-1),
                duration: started.elapsed(),
            };
        }
    }

    match run_git_command(&repo.path, &["pull", "--ff-only"]) {
        Ok(result) => {
            output.push_str(&result.body);
            RepoResult {
                output,
                success: true,
                exit_code: result.exit_code.unwrap_or(0),
                duration: started.elapsed(),
            }
        }
        Err(result) => {
            output.push_str(&result.body);
            RepoResult {
                output,
                success: false,
                exit_code: result.exit_code.unwrap_or(-1),
                duration: started.elapsed(),
            }
        }
    }
}

fn confirm_execution(repo_count: usize) -> anyhow::Result<()> {
    if !io::stdin().is_terminal() {
        anyhow::bail!(
            "Confirmation requested but stdin is not a TTY. Run without --confirm or use an interactive terminal."
        );
    }

    print!(
        "Proceed to run git status + pull on {repo_count} repositories? [y/N]: "
    );
    io::stdout().flush()?;

    let mut answer = String::new();
    io::stdin().read_line(&mut answer)?;
    let answer = answer.trim().to_lowercase();
    if answer == "y" || answer == "yes" {
        Ok(())
    } else {
        anyhow::bail!("Operation cancelled by user.")
    }
}

struct GitOutput {
    body: String,
    exit_code: Option<i32>,
}

fn run_git_command(repo_path: &Path, args: &[&str]) -> Result<GitOutput, GitOutput> {
    let output = Command::new("git")
        .args(args)
        .current_dir(repo_path)
        .output()
        .map_err(|err| GitOutput {
            body: format!("Failed to run git {}: {err}\n", args.join(" ")),
            exit_code: None,
        })?;

    let mut body = String::new();
    if !output.stdout.is_empty() {
        body.push_str(&String::from_utf8_lossy(&output.stdout));
    }
    if !output.stderr.is_empty() {
        body.push_str(&String::from_utf8_lossy(&output.stderr));
    }

    let result = GitOutput {
        body,
        exit_code: output.status.code(),
    };

    if output.status.success() {
        Ok(result)
    } else {
        Err(result)
    }
}

fn print_summary(
    repos: &[Repo],
    processed: &[(usize, RepoResult)],
    sort_by: SortBy,
    total_duration: Duration,
) {
    let mut rows: Vec<_> = processed
        .iter()
        .map(|(idx, result)| {
            let repo = &repos[*idx];
            (repo.name.clone(), result)
        })
        .collect();

    match sort_by {
        SortBy::Name => rows.sort_by(|a, b| a.0.cmp(&b.0)),
        SortBy::Duration => rows.sort_by(|a, b| a.1.duration.cmp(&b.1.duration)),
    }

    println!(
        "\nSummary (sorted by {}):",
        match sort_by {
            SortBy::Name => "name",
            SortBy::Duration => "duration",
        }
    );
    println!("{:<24} {:<8} {:<8} {:>10}", "repo", "status", "exit", "duration");

    for (name, result) in rows {
        let status = if result.success { "ok" } else { "err" };
        let millis = result.duration.as_millis();
        println!(
            "{:<24} {:<8} {:<8} {:>10}ms",
            name, status, result.exit_code, millis
        );
    }

    println!("Total time: {:.2?}", total_duration);
}
