1use crate::config::Config;
2use crate::github::cache::CacheConfig;
3use crate::github::types::PullRequest;
4use crate::scoring::{calculate_score, merge_scoring_configs, ScoreResult};
5use crate::snooze::{filter_active_prs, filter_snoozed_prs, SnoozeState};
6use anyhow::Result;
7use futures::stream::{FuturesUnordered, StreamExt};
8use std::collections::{HashMap, HashSet};
9use std::fmt;
10
11#[derive(Debug)]
15pub struct AuthError {
16 pub message: String,
17}
18
19impl fmt::Display for AuthError {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 write!(f, "{}", self.message)
22 }
23}
24
25impl std::error::Error for AuthError {}
26
27pub async fn fetch_and_score_prs(
33 client: &octocrab::Octocrab,
34 config: &Config,
35 snooze_state: &SnoozeState,
36 cache_config: &CacheConfig,
37 verbose: bool,
38 auth_username: Option<&str>,
39) -> Result<(
40 Vec<(PullRequest, ScoreResult)>,
41 Vec<(PullRequest, ScoreResult)>,
42 Option<u64>,
43)> {
44 if verbose {
45 let cache_status = if cache_config.enabled {
46 "enabled"
47 } else {
48 "disabled (--no-cache)"
49 };
50 eprintln!("Cache: {}", cache_status);
51 }
52
53 let global_scoring = config.scoring.clone().unwrap_or_default();
55
56 let mut all_prs = Vec::new();
58 let mut any_succeeded = false;
59
60 let mut futures = FuturesUnordered::new();
61 let auth_username_owned = auth_username.map(|s| s.to_string());
62 for (query_index, query_config) in config.queries.iter().enumerate() {
63 let client = client.clone();
64 let query = query_config.query.clone();
65 let query_name = query_config.name.clone();
66 let auth_username_clone = auth_username_owned.clone();
67 let merged_scoring = merge_scoring_configs(&global_scoring, query_config.scoring.as_ref());
69 let exclude_patterns = merged_scoring.size.and_then(|s| s.exclude);
70 futures.push(async move {
71 let result = crate::github::search_and_enrich_prs(
72 &client,
73 &query,
74 auth_username_clone.as_deref(),
75 exclude_patterns,
76 verbose,
77 )
78 .await;
79 (query_name, query, query_index, result)
80 });
81 }
82
83 while let Some((name, query, query_index, result)) = futures.next().await {
84 match result {
85 Ok(prs) => {
86 if verbose {
87 eprintln!(
88 " Found {} PRs for {}",
89 prs.len(),
90 name.as_deref().unwrap_or(&query)
91 );
92 }
93 all_prs.extend(prs.into_iter().map(|pr| (pr, query_index)));
95 any_succeeded = true;
96 }
97 Err(e) => {
98 if e.downcast_ref::<AuthError>().is_some() {
100 return Err(e);
101 }
102 if verbose {
103 eprintln!(
104 "Query failed: {} - {}",
105 name.as_deref().unwrap_or(&query),
106 e
107 );
108 }
109 }
110 }
111 }
112
113 if !any_succeeded && !config.queries.is_empty() {
115 anyhow::bail!("All queries failed. Check your network connection and GitHub token.");
116 }
117
118 let mut seen_urls = HashSet::new();
121 let mut pr_to_query_index = HashMap::new();
122 let unique_prs: Vec<_> = all_prs
123 .into_iter()
124 .filter_map(|(pr, query_idx)| {
125 if seen_urls.insert(pr.url.clone()) {
126 pr_to_query_index.insert(pr.url.clone(), query_idx);
127 Some(pr)
128 } else {
129 None
130 }
131 })
132 .collect();
133
134 if verbose {
135 eprintln!("After deduplication: {} unique PRs", unique_prs.len());
136 }
137
138 let active_prs = filter_active_prs(unique_prs.clone(), snooze_state);
140 let snoozed_prs = filter_snoozed_prs(unique_prs, snooze_state);
141
142 if verbose {
143 eprintln!(
144 "After filter: {} active, {} snoozed",
145 active_prs.len(),
146 snoozed_prs.len()
147 );
148 }
149
150 let mut active_scored: Vec<_> = active_prs
152 .into_iter()
153 .map(|pr| {
154 let query_idx = pr_to_query_index.get(&pr.url).copied().unwrap_or(0);
156 let scoring =
157 merge_scoring_configs(&global_scoring, config.queries[query_idx].scoring.as_ref());
158 let result = calculate_score(&pr, &scoring);
159 (pr, result)
160 })
161 .collect();
162
163 let mut snoozed_scored: Vec<_> = snoozed_prs
165 .into_iter()
166 .map(|pr| {
167 let query_idx = pr_to_query_index.get(&pr.url).copied().unwrap_or(0);
169 let scoring =
170 merge_scoring_configs(&global_scoring, config.queries[query_idx].scoring.as_ref());
171 let result = calculate_score(&pr, &scoring);
172 (pr, result)
173 })
174 .collect();
175
176 let sort_fn = |a: &(PullRequest, ScoreResult), b: &(PullRequest, ScoreResult)| {
178 let score_cmp =
180 b.1.score
181 .partial_cmp(&a.1.score)
182 .unwrap_or(std::cmp::Ordering::Equal);
183 if score_cmp != std::cmp::Ordering::Equal {
184 return score_cmp;
185 }
186 a.0.created_at.cmp(&b.0.created_at)
188 };
189
190 active_scored.sort_by(sort_fn);
191 snoozed_scored.sort_by(sort_fn);
192
193 let rate_limit_remaining = match client.ratelimit().get().await {
195 Ok(rate_limit) => Some(rate_limit.resources.core.remaining as u64),
196 Err(_) => None,
197 };
198
199 Ok((active_scored, snoozed_scored, rate_limit_remaining))
200}