i-self 0.4.3

Personal developer-companion CLI: scans your repos, indexes code semantically, watches your activity, and moves AI-agent sessions between tools (Claude Code, Aider, Goose, OpenAI Codex CLI, Continue.dev, OpenCode).
#![allow(dead_code)]

use super::{types::*, GitHubClient};
use anyhow::Result;
use chrono::{Duration, Utc};
use futures::future::join_all;
use std::sync::Arc;
use tokio::sync::Semaphore;
use tracing::{error, info, warn};

/// Maximum concurrent repository scans
const MAX_CONCURRENT_SCANS: usize = 5;

/// Scans GitHub repositories and extracts developer activity
pub struct GitHubScanner {
    client: Arc<GitHubClient>,
    semaphore: Arc<Semaphore>,
}

impl GitHubScanner {
    pub fn new(client: GitHubClient) -> Self {
        Self {
            client: Arc::new(client),
            semaphore: Arc::new(Semaphore::new(MAX_CONCURRENT_SCANS)),
        }
    }

    /// Scan all repositories for the authenticated user
    pub async fn scan_all_repos(&self) -> Result<Vec<RepositoryScan>> {
        info!("Starting full GitHub repository scan");
        
        let repos = self.client.get_all_repos().await?;
        info!("Found {} repositories to scan", repos.len());

        let mut handles = Vec::new();

        for repo in repos {
            let client = self.client.clone();
            let permit = self.semaphore.clone();
            
            let handle = tokio::spawn(async move {
                let _permit = permit.acquire().await?;
                Self::scan_single_repo(&client, repo).await
            });

            handles.push(handle);
        }

        let results = join_all(handles).await;
        let mut scans = Vec::new();

        for result in results {
            match result {
                Ok(Ok(scan)) => scans.push(scan),
                Ok(Err(e)) => warn!("Failed to scan repository: {}", e),
                Err(e) => error!("Task panicked: {}", e),
            }
        }

        info!("Completed scanning {} repositories", scans.len());
        Ok(scans)
    }

    /// Scan a single repository in detail
    async fn scan_single_repo(client: &GitHubClient, repo: Repository) -> Result<RepositoryScan> {
        info!("Scanning repository: {}/{}", repo.owner, repo.name);
        
        let owner = repo.owner.clone();
        let repo_name = repo.name.clone();

        // Get branches
        let branches = match client.get_branches(&owner, &repo_name).await {
            Ok(branches) => branches,
            Err(e) => {
                warn!("Failed to get branches for {}: {}", repo_name, e);
                Vec::new()
            }
        };

        // Get recent commits (last 30 days)
        let since = Utc::now() - Duration::days(30);
        let mut all_commits = Vec::new();
        
        for branch in &branches {
            if let Ok(commits) = client.get_recent_commits(&owner, &repo_name, &branch.name, Some(since)).await {
                all_commits.extend(commits);
            }
        }

        // Get language stats
        let language_stats = match client.get_language_stats(&owner, &repo_name).await {
            Ok(stats) => stats,
            Err(e) => {
                warn!("Failed to get language stats for {}: {}", repo_name, e);
                LanguageStats {
                    languages: Default::default(),
                    total_bytes: 0,
                }
            }
        };

        // Get recent PRs
        let pull_requests = match client.get_recent_prs(&owner, &repo_name, "all").await {
            Ok(prs) => prs.into_iter()
                .filter(|pr| pr.updated_at > since)
                .collect(),
            Err(e) => {
                warn!("Failed to get PRs for {}: {}", repo_name, e);
                Vec::new()
            }
        };

        Ok(RepositoryScan {
            repository: repo,
            branches,
            recent_commits: all_commits,
            language_stats,
            pull_requests,
            scan_timestamp: Utc::now(),
        })
    }

    /// Generate a summary of GitHub activity
    pub async fn generate_summary(&self, scans: &[RepositoryScan]) -> GitHubSummary {
        let mut summary = GitHubSummary::default();
        
        summary.total_repos = scans.len();
        summary.total_branches = scans.iter().map(|s| s.branches.len()).sum();
        summary.total_commits_30d = scans.iter().map(|s| s.recent_commits.len()).sum();
        summary.total_prs_open = scans.iter()
            .map(|s| s.pull_requests.iter().filter(|pr| pr.state == "open").count())
            .sum();
        summary.total_prs_merged_30d = scans.iter()
            .map(|s| s.pull_requests.iter()
                .filter(|pr| pr.state == "closed" && pr.merged_at.is_some())
                .count())
            .sum();

        // Aggregate language stats
        let mut language_bytes: std::collections::HashMap<String, i64> = std::collections::HashMap::new();
        for scan in scans {
            for (lang, (bytes, _)) in &scan.language_stats.languages {
                *language_bytes.entry(lang.clone()).or_insert(0) += bytes;
            }
        }

        // Sort languages by bytes
        let mut languages: Vec<_> = language_bytes.into_iter().collect();
        languages.sort_by(|a, b| b.1.cmp(&a.1));
        summary.top_languages = languages.into_iter().take(10).collect();

        // Find most active repos by commit count
        let mut repo_activity: Vec<_> = scans.iter()
            .map(|s| (s.repository.full_name.clone(), s.recent_commits.len()))
            .collect();
        repo_activity.sort_by(|a, b| b.1.cmp(&a.1));
        summary.most_active_repos = repo_activity.into_iter()
            .take(5)
            .map(|(name, _)| name)
            .collect();

        // Generate recent activity events
        for scan in scans {
            // Add recent commits as events
            for commit in scan.recent_commits.iter().take(5) {
                if let Some(date) = commit.committer_date {
                    summary.recent_activity.push(ActivityEvent {
                        event_type: "commit".to_string(),
                        repo: scan.repository.full_name.clone(),
                        description: format!("{}: {}", 
                            commit.author_name.as_deref().unwrap_or("Unknown"),
                            commit.message.lines().next().unwrap_or("").chars().take(50).collect::<String>()),
                        timestamp: date,
                        url: None,
                    });
                }
            }

            // Add recent PRs as events
            for pr in scan.pull_requests.iter().take(3) {
                summary.recent_activity.push(ActivityEvent {
                    event_type: format!("pr_{}", pr.state),
                    repo: scan.repository.full_name.clone(),
                    description: format!("#{}: {}", pr.number, pr.title),
                    timestamp: pr.updated_at,
                    url: None,
                });
            }
        }

        // Sort activity by timestamp
        summary.recent_activity.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
        summary.recent_activity.truncate(20);

        summary
    }

    /// Get repositories that need attention (stale branches, old PRs, etc.)
    pub async fn get_attention_needed(&self, scans: &[RepositoryScan]) -> Vec<String> {
        let mut attention_items = Vec::new();
        let now = Utc::now();

        for scan in scans {
            // Check for stale branches (no commits in 90 days)
            for branch in &scan.branches {
                if let Some(last_commit) = branch.last_commit_date {
                    if now.signed_duration_since(last_commit).num_days() > 90 
                        && branch.name != scan.repository.default_branch {
                        attention_items.push(format!(
                            "{}: Branch '{}' is stale (last commit {} days ago)",
                            scan.repository.full_name,
                            branch.name,
                            now.signed_duration_since(last_commit).num_days()
                        ));
                    }
                }
            }

            // Check for old open PRs
            for pr in &scan.pull_requests {
                if pr.state == "open" {
                    let age = now.signed_duration_since(pr.created_at).num_days();
                    if age > 30 {
                        attention_items.push(format!(
                            "{}: PR #{} '{}' is open for {} days",
                            scan.repository.full_name,
                            pr.number,
                            pr.title,
                            age
                        ));
                    }
                }
            }
        }

        attention_items
    }
}