//! Implements `limb update`. Fetch + fast-forward every worktree.

use std::path::{Path, PathBuf};
use std::sync::Mutex;

use anyhow::Result;
use rayon::prelude::*;
use serde::Serialize;

use crate::cli::UpdateArgs;
use crate::context::Context;
use crate::discover;
use crate::worktree;

/// Runs `limb update`.
///
/// Single-repo (default): `git fetch --all --prune` once, then
/// `merge --ff-only @{upstream}` in each non-bare worktree that has a
/// branch. `--fetch-only` skips the fast-forward pass; `--ff-only` skips
/// the fetch. Fast-forward failures are reported as `skip` and do not
/// abort.
///
/// Cross-repo (`--all`): fetches every repo under `projects.roots` in
/// parallel via rayon, then per-repo fast-forwards each non-bare
/// worktree with a branch. Prints a summary table at the end (or JSON
/// with `--json`).
///
/// # Errors
///
/// Returns an error if the repo (or global config, with `--all`) cannot
/// be resolved or the initial fetch fails. Per-worktree fast-forward
/// failures are non-fatal.
pub fn run(ctx: &Context, args: &UpdateArgs) -> Result<()> {
    if args.all {
        run_all(ctx, args)
    } else {
        run_single(ctx, args)
    }
}

fn run_single(ctx: &Context, args: &UpdateArgs) -> Result<()> {
    let repo = ctx.repo()?;
    let result = update_one_repo(&repo, args, ctx.quiet, &Mutex::new(()));
    if let Some(e) = result.fatal {
        return Err(e);
    }
    Ok(())
}

fn run_all(ctx: &Context, args: &UpdateArgs) -> Result<()> {
    let global = ctx.global()?;
    if global.projects_roots.is_empty() {
        anyhow::bail!(
            "--all: no projects.roots configured; \
             try: set `projects.roots = [\"~/dev/myrepos\"]` in ~/.config/limb/config.toml"
        );
    }
    let repos: Vec<PathBuf> = global
        .projects_roots
        .iter()
        .filter(|r| r.is_dir())
        .flat_map(|r| discover::repos_under(r).unwrap_or_default())
        .collect();

    if repos.is_empty() {
        anstream::eprintln!("no repos found under configured projects.roots");
        return Ok(());
    }

    let stderr_lock = Mutex::new(());
    let results: Vec<UpdateResult> = repos
        .par_iter()
        .map(|repo_dir| update_one_repo(repo_dir, args, ctx.quiet, &stderr_lock))
        .collect();

    if ctx.json {
        println!("{}", serde_json::to_string_pretty(&results)?);
        return Ok(());
    }

    print_summary(&results);
    if results.iter().any(|r| r.fatal.is_some()) {
        anyhow::bail!("one or more repos failed to fetch");
    }
    Ok(())
}

#[derive(Debug, Serialize)]
struct UpdateResult {
    repo: String,
    fetched: bool,
    worktrees: Vec<WorktreeResult>,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<String>,
    #[serde(skip)]
    fatal: Option<anyhow::Error>,
}

#[derive(Debug, Serialize)]
struct WorktreeResult {
    name: String,
    status: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    reason: Option<String>,
}

fn update_one_repo(
    repo_dir: &Path,
    args: &UpdateArgs,
    quiet: bool,
    stderr_lock: &Mutex<()>,
) -> UpdateResult {
    let dim = crate::style::DIM;
    let ok_s = crate::style::OK;
    let warn = crate::style::WARN;
    let accent = crate::style::ACCENT;

    let name = discover::repo_name(repo_dir);
    let anchor = discover::anchor_for(repo_dir).unwrap_or_else(|| repo_dir.to_path_buf());

    let mut result = UpdateResult {
        repo: name.clone(),
        fetched: false,
        worktrees: Vec::new(),
        error: None,
        fatal: None,
    };

    if !args.ff_only {
        if args.dry_run {
            if !quiet {
                let _g = stderr_lock.lock();
                anstream::eprintln!("{dim}{accent}{name}{accent:#} would fetch{dim:#}");
            }
        } else {
            match worktree::fetch(&anchor, true) {
                Ok(()) => {
                    result.fetched = true;
                    if !quiet {
                        let _g = stderr_lock.lock();
                        anstream::eprintln!("{accent}{name}{accent:#} {ok_s}fetched{ok_s:#}");
                    }
                }
                Err(e) => {
                    result.error = Some(format!("{e:#}"));
                    if !quiet {
                        let _g = stderr_lock.lock();
                        anstream::eprintln!(
                            "{accent}{name}{accent:#} {warn}fetch failed{warn:#} ({e:#})"
                        );
                    }
                    result.fatal = Some(e);
                    return result;
                }
            }
        }
    }

    if args.fetch_only {
        return result;
    }

    let trees = match worktree::list(&anchor) {
        Ok(t) => t,
        Err(e) => {
            result.error = Some(format!("{e:#}"));
            result.fatal = Some(e);
            return result;
        }
    };

    for w in &trees {
        if w.bare || w.branch.is_none() {
            continue;
        }
        if args.dry_run {
            if !quiet {
                let _g = stderr_lock.lock();
                anstream::eprintln!("{dim}{accent}{name}{accent:#} would ff {}{dim:#}", w.name);
            }
            result.worktrees.push(WorktreeResult {
                name: w.name.clone(),
                status: "would-ff",
                reason: None,
            });
            continue;
        }
        match worktree::fast_forward(&w.path, true) {
            Ok(()) => {
                result.worktrees.push(WorktreeResult {
                    name: w.name.clone(),
                    status: "ok",
                    reason: None,
                });
            }
            Err(e) => {
                result.worktrees.push(WorktreeResult {
                    name: w.name.clone(),
                    status: "skip",
                    reason: Some(format!("{e:#}")),
                });
            }
        }
    }
    result
}

fn print_summary(results: &[UpdateResult]) {
    let dim = crate::style::DIM;
    let ok_s = crate::style::OK;
    let warn = crate::style::WARN;
    let err = crate::style::ERROR;

    let total_fetched = results.iter().filter(|r| r.fetched).count();
    let total_failed = results.iter().filter(|r| r.fatal.is_some()).count();
    let total_ff: usize = results
        .iter()
        .map(|r| r.worktrees.iter().filter(|w| w.status == "ok").count())
        .sum();
    let total_skip: usize = results
        .iter()
        .map(|r| r.worktrees.iter().filter(|w| w.status == "skip").count())
        .sum();

    anstream::eprintln!();
    anstream::eprintln!(
        "{dim}{} repos · {ok_s}{total_fetched} fetched{ok_s:#}{dim} · \
         {ok_s}{total_ff} ff{ok_s:#}{dim} · {warn}{total_skip} skip{warn:#}{dim} · \
         {err}{total_failed} failed{err:#}{dim:#}",
        results.len()
    );
}
