//! List command for displaying worktrees and branches.
//!
//! # Performance
//!
//! `wt list` runs multiple git commands per worktree in parallel using Rayon. Performance
//! depends heavily on git's internal caches, not worktrunk-specific caching.
//!
//! ## Time to First Information
//!
//! Before displaying actual data, we perform these sequential operations:
//!
//! 1. **`git worktree list --porcelain`** (~5-15ms)
//!    - Lists all worktrees with their paths, HEADs, and branches
//!    - Uses ref cache, very fast
//!
//! 2. **Default branch lookup** (~1-5ms)
//!    - Reads cached value from `git config worktrunk.default-branch`
//!    - Falls back to git's remote HEAD ref (e.g., `origin/HEAD`) if available
//!    - If still unknown, queries `git ls-remote --symref <remote> HEAD` (~100ms-2s network)
//!    - Clear cache with `wt config state default-branch clear` to force re-detection
//!
//! 3. **Sort worktrees** (<1ms)
//!    - Orders by: current → main → rest by timestamp (most recent first)
//!    - Pure Rust, no git calls
//!
//! 4. **Branch listing** (only with `--branches`, ~10-30ms)
//!    - `git for-each-ref refs/heads` - lists local branches
//!    - Filters out branches that already have worktrees
//!
//! 5. **Remote branch listing** (only with `--remotes`, ~10-50ms)
//!    - `git for-each-ref refs/remotes` - lists remote branches
//!    - Scales with number of remotes/branches
//!
//! 6. **Layout calculation** (<1ms)
//!    - Determines column widths from branch names and paths
//!    - Pure Rust, no git calls
//!
//! **Time to skeleton: ~50ms** (placeholder rows with loading indicators)
//!
//! Skeleton timing is stable at ~46-52ms regardless of worktree count (1-8) or cache state.
//! This is because skeleton operations (worktree list, default branch lookup, layout) use
//! git's ref cache rather than packed-refs or the index.
//!
//! **Time to complete: ~60-170ms** (all data filled in, depends on worktree count)
//!
//! First run in a repo without cached default branch adds ~100-300ms for network lookup.
//!
//! After the skeleton appears, cells fill in progressively as git operations complete.
//! The slowest operation (CI status) only runs with `--full`.
//!
//! ## Git Commands Per Worktree
//!
//! For each worktree, we execute:
//! - `git status --porcelain` - Working tree state (uses index cache)
//! - `git rev-list --count <base>..<head>` - Ahead/behind counts (uses commit graph)
//! - `git diff --shortstat HEAD` - Working tree line diffs (uses index + tree objects)
//! - `git diff --shortstat <base>...<head>` - Branch line diffs (uses tree objects)
//! - `git rev-parse <ref>` - Ref resolution (uses ref cache)
//!
//! Plus one global command:
//! - `git worktree list --porcelain` - List all worktrees (uses ref cache)
//!
//! ## Git's Internal Caches
//!
//! Git caches significantly affect performance. These caches are maintained by git itself:
//!
//! 1. **Index (`.git/index`)** - Cached file metadata (mtime, size, mode)
//!    - Speeds up `git status` by avoiding full file content comparisons
//!    - Invalidated when files change or staging area updates
//!    - Cold: ~100ms per worktree, Warm: ~10ms per worktree (typical repo)
//!
//! 2. **Commit graph (`.git/objects/info/commit-graph`)** - Precomputed commit metadata
//!    - Speeds up `git rev-list --count` by avoiding commit object parsing
//!    - Generated by `git commit-graph write` (some repos auto-generate)
//!    - Without: O(commits) parsing, With: O(1) metadata lookup
//!    - Cold: ~50ms for 1000 commits, Warm: ~5ms (with commit graph)
//!
//! 3. **Ref cache (`.git/refs/` and `.git/packed-refs`)** - Cached ref resolutions
//!    - Speeds up `git rev-parse` and branch lookups
//!    - Packed refs improve performance in repos with many branches
//!    - Cold: ~10ms, Warm: <1ms
//!
//! 4. **Filesystem cache (OS-level)** - Recently accessed files stay in memory
//!    - Speeds up all git operations after first access
//!    - Cleared when system is under memory pressure
//!    - Most impactful for large repos with many pack files
//!
//! 5. **Pack files (`.git/objects/pack/`)** - Compressed object storage
//!    - Faster than loose objects for bulk access
//!    - `git gc` consolidates loose objects into packs
//!    - More efficient for tree/blob access in diffs
//!
//! ## Worktrunk's Only Cache: Default Branch
//!
//! Worktrunk caches only the default branch name (main/master) in
//! `git config worktrunk.default-branch`. The remote HEAD ref (e.g., `origin/HEAD`)
//! is git's cache; worktrunk reads it but does not set it. All other data is fetched
//! fresh on each `wt list` invocation.
//!
//! Clear cache with: `wt config state default-branch clear`
//!
//! ## Performance Characteristics
//!
//! Scaling (from benches/list.rs):
//! - Linear with worktree count due to parallelization (Rayon thread pool)
//! - Dominated by git command overhead, not Rust code
//! - Cold caches: ~150-300ms per worktree (typical repo, 500 commits, 100 files)
//! - Warm caches: ~20-50ms per worktree
//! - Real-world: rust-lang/rust repo with 8 worktrees: ~400ms (warm caches)
//!
//! Bottlenecks:
//! 1. `git status --porcelain` - Slowest when index is cold or many files changed
//! 2. `git rev-list --count` - Slow without commit graph in repos with deep history
//! 3. `git diff --shortstat` - Slow for large diffs or when pack files aren't cached
//!
//! Optimization tips:
//! - Run `git commit-graph write --reachable --changed-paths` to speed up commit counting
//! - Run `git gc` periodically to consolidate objects into pack files
//! - Minimize uncommitted changes across worktrees (each dirty worktree adds diff overhead)

pub mod ci_status;
pub(crate) mod collect;
pub(crate) mod columns;
pub(crate) mod custom_columns;
pub mod json_output;
pub mod json_v2;
pub(crate) mod layout;
pub mod model;
pub mod progressive;
mod progressive_table;
pub(crate) mod render;

// Layout is calculated in collect/mod.rs
use anstyle::Style;
use anyhow::Context;
use model::{ListData, ListItem};
use progressive::RenderTarget;
use worktrunk::git::Repository;
use worktrunk::styling::INFO_SYMBOL;

// Re-export for statusline and other consumers
pub use collect::{CollectOptions, build_worktree_item, populate_item};
pub use model::StatuslineSegment;

pub fn handle_list(
    repo: Repository,
    format: crate::OutputFormat,
    cli_branches: bool,
    cli_remotes: bool,
    cli_full: bool,
    progressive_flag: Option<bool>,
) -> anyhow::Result<()> {
    let render_target = RenderTarget::detect(format, progressive_flag);

    // Resolve the JSON schema before collecting, so the unset-nag lands
    // above the output rather than after a long collection.
    let json_schema =
        matches!(render_target, RenderTarget::Json).then(|| resolve_json_schema(&repo));

    let list_data = collect::collect(
        &repo,
        collect::ShowConfig::DeferredToParallel {
            cli_branches,
            cli_remotes,
            cli_full,
        },
        render_target,
    )?;

    let Some(ListData {
        items,
        custom_columns,
        collected,
    }) = list_data
    else {
        return Ok(());
    };

    match json_schema {
        // Table modes already rendered inside `collect()`.
        None => {}
        Some(2) => print_json(&json_v2::to_json_envelope(
            &items,
            &custom_columns,
            &repo,
            collected,
        ))?,
        Some(_) => print_json(&json_output::to_json_items(&items, &custom_columns, &repo))?,
    }

    Ok(())
}

/// Serialize a JSON answer to stdout (pretty, one trailing newline).
pub(crate) fn print_json<T: serde::Serialize>(value: &T) -> anyhow::Result<()> {
    let json = serde_json::to_string_pretty(value).context("Failed to serialize to JSON")?;
    println!("{}", json);
    Ok(())
}

/// Resolve `[list] json-schema` (per-project resolved config) to 1 or 2.
///
/// Unset defaults to schema 1 and nags once per process; an out-of-range
/// value warns and defaults to schema 1, matching how config load treats a
/// type error in the same key (warn and degrade, never brick a command).
/// Both messages honor warning suppression — on the statusline, stderr
/// would corrupt the consumer's prompt, and the same user sees the nag on
/// their next interactive run.
///
/// The unset state is the `PendingDefault` row in `DEPRECATION_RULES`, but
/// its warning fires here rather than at config load: the setting only
/// matters to JSON consumers, so a load-time warning would nag every command
/// for every user without the key. `wt config update` writes the upcoming
/// `json-schema = 2` (adopting the new schema is the migration; staying on
/// schema 1 is the deliberate manual edit), so the nag's hint offers that
/// command exactly when running it would write the key — decided by the same
/// detection update runs, so a missing, unreadable, or malformed user config
/// falls back to naming the manual setting instead.
pub(crate) fn resolve_json_schema(repo: &Repository) -> u8 {
    use std::sync::Once;

    use color_print::cformat;
    use worktrunk::styling::{eprintln, hint_message, warning_message};

    static WARNED: Once = Once::new();
    match repo.config().list.json_schema {
        Some(v @ (1 | 2)) => v,
        Some(other) => {
            WARNED.call_once(|| {
                if worktrunk::config::warnings_suppressed() {
                    return;
                }
                eprintln!(
                    "{}",
                    warning_message(cformat!(
                        "[list] json-schema is <bold>{other}</>, expected 1 or 2; using schema 1"
                    ))
                );
            });
            1
        }
        None => {
            WARNED.call_once(|| {
                if worktrunk::config::warnings_suppressed() {
                    return;
                }
                eprintln!(
                    "{}",
                    warning_message(
                        "JSON output is schema 1; a future release switches the default to schema 2"
                    )
                );
                let update_would_adopt = worktrunk::config::config_path()
                    .and_then(|p| std::fs::read_to_string(p).ok())
                    .is_some_and(|content| {
                        worktrunk::config::detect_deprecations(
                            &content,
                            worktrunk::config::ConfigFileKind::User,
                        )
                        .iter()
                        .any(|k| matches!(k, worktrunk::config::DeprecationKind::JsonSchemaUnset))
                    });
                let adopt = if update_would_adopt {
                    cformat!("run <underline>wt config update</>")
                } else {
                    cformat!("set <underline>json-schema = 2</>")
                };
                eprintln!(
                    "{}",
                    hint_message(cformat!(
                        "To keep this format set <underline>[list] json-schema = 1</>; to adopt the new schema, {adopt}"
                    ))
                );
            });
            1
        }
    }
}

#[derive(Default)]
pub(super) struct SummaryMetrics {
    worktrees: usize,
    local_branches: usize,
    remote_branches: usize,
    dirty_worktrees: usize,
    ahead_items: usize,
}

impl SummaryMetrics {
    pub(super) fn from_items(items: &[ListItem]) -> Self {
        let mut metrics = Self::default();
        for item in items {
            metrics.update(item);
        }
        metrics
    }

    fn update(&mut self, item: &ListItem) {
        if let Some(_data) = item.worktree_data() {
            self.worktrees += 1;
            // Use status_symbols.working_tree which includes untracked files,
            // not just working_tree_diff which only has tracked changes.
            // `None` means gate 1 hasn't resolved yet — don't count.
            if item
                .status_symbols
                .working_tree
                .is_some_and(|wt| wt.is_dirty())
            {
                self.dirty_worktrees += 1;
            }
        } else {
            // Distinguish local vs remote branches by presence of '/' in name
            // Remote branches are like "origin/feature", local are like "feature"
            if item.branch.as_ref().is_some_and(|b| b.contains('/')) {
                self.remote_branches += 1;
            } else {
                self.local_branches += 1;
            }
        }

        if item.counts.is_some_and(|c| c.ahead > 0) {
            self.ahead_items += 1;
        }
    }

    pub(super) fn summary_parts(
        &self,
        include_branches: bool,
        hidden_columns: usize,
    ) -> Vec<String> {
        let mut parts = Vec::new();

        let plural = if self.worktrees == 1 { "" } else { "s" };
        parts.push(format!("{} worktree{}", self.worktrees, plural));

        if include_branches {
            if self.local_branches > 0 {
                let plural = if self.local_branches == 1 { "" } else { "es" };
                parts.push(format!("{} branch{}", self.local_branches, plural));
            }
            if self.remote_branches > 0 {
                let plural = if self.remote_branches == 1 { "" } else { "es" };
                parts.push(format!("{} remote branch{}", self.remote_branches, plural));
            }
        }

        if self.dirty_worktrees > 0 {
            parts.push(format!("{} with changes", self.dirty_worktrees));
        }

        if self.ahead_items > 0 {
            parts.push(format!("{} ahead", self.ahead_items));
        }

        if hidden_columns > 0 {
            let plural = if hidden_columns == 1 {
                "column"
            } else {
                "columns"
            };
            parts.push(format!("{} {} hidden", hidden_columns, plural));
        }

        parts
    }
}

/// Format the summary line that closes the table.
///
/// The footer is the only home a timed-out task has: it leaves an empty cell
/// and no message to print, so the count here is all that reports it. Tasks
/// that failed instead get a named entry in the warning that follows the
/// table, and that warning carries its own count — repeating it here would
/// print the same number twice on adjacent lines.
pub(crate) fn format_summary_message(
    items: &[ListItem],
    show_branches: bool,
    hidden_column_count: usize,
    timed_out_count: usize,
) -> String {
    let metrics = SummaryMetrics::from_items(items);
    let dim = Style::new().dimmed();
    let summary = metrics
        .summary_parts(show_branches, hidden_column_count)
        .join(", ");

    if timed_out_count > 0 {
        let plural = if timed_out_count == 1 { "" } else { "s" };
        format!(
            "{INFO_SYMBOL} {dim}Showing {summary}; {timed_out_count} task{plural} timed out{dim:#}"
        )
    } else {
        format!("{INFO_SYMBOL} {dim}Showing {summary}{dim:#}")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_summary_metrics_default() {
        let metrics = SummaryMetrics::default();
        assert_eq!(metrics.worktrees, 0);
        assert_eq!(metrics.local_branches, 0);
        assert_eq!(metrics.remote_branches, 0);
        assert_eq!(metrics.dirty_worktrees, 0);
        assert_eq!(metrics.ahead_items, 0);
    }

    #[test]
    fn test_summary_metrics_summary_parts_single_worktree() {
        let metrics = SummaryMetrics {
            worktrees: 1,
            local_branches: 0,
            remote_branches: 0,
            dirty_worktrees: 0,
            ahead_items: 0,
        };
        let parts = metrics.summary_parts(false, 0);
        assert_eq!(parts, vec!["1 worktree"]);
    }

    #[test]
    fn test_summary_metrics_summary_parts_multiple_worktrees() {
        let metrics = SummaryMetrics {
            worktrees: 3,
            local_branches: 0,
            remote_branches: 0,
            dirty_worktrees: 0,
            ahead_items: 0,
        };
        let parts = metrics.summary_parts(false, 0);
        assert_eq!(parts, vec!["3 worktrees"]);
    }

    #[test]
    fn test_summary_metrics_summary_parts_with_branches() {
        let metrics = SummaryMetrics {
            worktrees: 2,
            local_branches: 5,
            remote_branches: 10,
            dirty_worktrees: 0,
            ahead_items: 0,
        };
        let parts = metrics.summary_parts(true, 0);
        assert_eq!(
            parts,
            vec!["2 worktrees", "5 branches", "10 remote branches"]
        );
    }

    #[test]
    fn test_summary_metrics_summary_parts_with_dirty() {
        let metrics = SummaryMetrics {
            worktrees: 3,
            local_branches: 0,
            remote_branches: 0,
            dirty_worktrees: 2,
            ahead_items: 0,
        };
        let parts = metrics.summary_parts(false, 0);
        assert_eq!(parts, vec!["3 worktrees", "2 with changes"]);
    }

    #[test]
    fn test_summary_metrics_summary_parts_with_ahead() {
        let metrics = SummaryMetrics {
            worktrees: 2,
            local_branches: 0,
            remote_branches: 0,
            dirty_worktrees: 0,
            ahead_items: 1,
        };
        let parts = metrics.summary_parts(false, 0);
        assert_eq!(parts, vec!["2 worktrees", "1 ahead"]);
    }

    #[test]
    fn test_summary_metrics_summary_parts_with_hidden_columns() {
        let metrics = SummaryMetrics {
            worktrees: 1,
            local_branches: 0,
            remote_branches: 0,
            dirty_worktrees: 0,
            ahead_items: 0,
        };
        let parts = metrics.summary_parts(false, 1);
        assert_eq!(parts, vec!["1 worktree", "1 column hidden"]);

        let parts = metrics.summary_parts(false, 3);
        assert_eq!(parts, vec!["1 worktree", "3 columns hidden"]);
    }

    #[test]
    fn test_summary_metrics_summary_parts_branches_no_local() {
        let metrics = SummaryMetrics {
            worktrees: 2,
            local_branches: 0,
            remote_branches: 5,
            dirty_worktrees: 0,
            ahead_items: 0,
        };
        let parts = metrics.summary_parts(true, 0);
        assert_eq!(parts, vec!["2 worktrees", "5 remote branches"]);
    }

    #[test]
    fn test_summary_metrics_summary_parts_all_features() {
        let metrics = SummaryMetrics {
            worktrees: 5,
            local_branches: 3,
            remote_branches: 8,
            dirty_worktrees: 2,
            ahead_items: 4,
        };
        let parts = metrics.summary_parts(true, 2);
        assert_eq!(
            parts,
            vec![
                "5 worktrees",
                "3 branches",
                "8 remote branches",
                "2 with changes",
                "4 ahead",
                "2 columns hidden"
            ]
        );
    }

    #[test]
    fn test_format_summary_message_timeout_variants() {
        use insta::assert_snapshot;

        // Nothing timed out. Failures alone leave the footer untouched — the
        // warning after the table names them and carries their count.
        assert_snapshot!(format_summary_message(&[], false, 0, 0), @"[2m○[22m [2mShowing 0 worktrees[0m");
        // Single timeout
        assert_snapshot!(format_summary_message(&[], false, 0, 1), @"[2m○[22m [2mShowing 0 worktrees; 1 task timed out[0m");
        // Several timeouts
        assert_snapshot!(format_summary_message(&[], false, 0, 3), @"[2m○[22m [2mShowing 0 worktrees; 3 tasks timed out[0m");
    }
}
