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).
use super::{ShellPreferences, CommandFrequency, CommandHistoryStats, WorkflowPatterns};
use anyhow::Result;
use chrono::{DateTime, Utc, Duration};
use std::collections::HashMap;
use tracing::{info, debug};

pub struct ShellHistoryAnalyzer;

impl ShellHistoryAnalyzer {
    pub fn new() -> Self {
        Self
    }

    pub async fn analyze(&self) -> Result<ShellPreferences> {
        let shell = self.detect_shell()?;
        let history_path = self.get_history_path(&shell);
        
        let mut prefs = ShellPreferences {
            shell_type: shell,
            shell_config_path: self.get_config_path(),
            prompt_style: None,
            favorite_commands: Vec::new(),
            command_history_stats: CommandHistoryStats::default(),
        };

        if let Some(path) = &history_path {
            if let Ok(stats) = self.analyze_history_file(path).await {
                prefs.command_history_stats = stats;
            }
        }

        // Detect prompt style from config
        prefs.prompt_style = self.detect_prompt_style().await.ok();

        Ok(prefs)
    }

    fn detect_shell(&self) -> Result<String> {
        if let Ok(shell) = std::env::var("SHELL") {
            let shell_name = std::path::Path::new(&shell)
                .file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("unknown")
                .to_string();
            return Ok(shell_name);
        }
        
        // Fallback detection
        if std::env::var("ZSH_VERSION").is_ok() {
            Ok("zsh".to_string())
        } else if std::env::var("BASH_VERSION").is_ok() {
            Ok("bash".to_string())
        } else {
            Ok("sh".to_string())
        }
    }

    fn get_history_path(&self, shell: &str) -> Option<String> {
        let home = dirs::home_dir()?;
        
        match shell {
            "zsh" => Some(home.join(".zsh_history").to_string_lossy().to_string()),
            "bash" => Some(home.join(".bash_history").to_string_lossy().to_string()),
            "fish" => Some(home.join(".local/share/fish/fish_history").to_string_lossy().to_string()),
            _ => None,
        }
    }

    fn get_config_path(&self) -> Option<String> {
        let home = dirs::home_dir()?;
        let shell = self.detect_shell().ok()?;
        
        match shell.as_str() {
            "zsh" => Some(home.join(".zshrc").to_string_lossy().to_string()),
            "bash" => Some(home.join(".bashrc").to_string_lossy().to_string()),
            "fish" => Some(home.join(".config/fish/config.fish").to_string_lossy().to_string()),
            _ => None,
        }
    }

    async fn analyze_history_file(&self, path: &str) -> Result<CommandHistoryStats> {
        use tokio::fs;
        
        let content = fs::read_to_string(path).await?;
        let lines: Vec<&str> = content.lines().collect();
        
        let mut command_counts: HashMap<String, i64> = HashMap::new();
        let mut hourly_distribution: HashMap<u8, i64> = HashMap::new();
        
        for line in &lines {
            // Parse command (simplified - real implementation would handle timestamps)
            let command = line.split_whitespace().next().unwrap_or("");
            if !command.is_empty() && !command.starts_with('#') {
                *command_counts.entry(command.to_string()).or_insert(0) += 1;
            }
        }

        let total_commands = lines.len() as i64;
        let unique_commands = command_counts.len() as i64;
        
        // Get most used commands
        let mut commands: Vec<_> = command_counts.into_iter().collect();
        commands.sort_by(|a, b| b.1.cmp(&a.1));
        let most_used: Vec<String> = commands.into_iter()
            .take(20)
            .map(|(cmd, _)| cmd)
            .collect();

        // Calculate typical working hours (simplified)
        let typical_hours: Vec<u8> = (9..=18).collect(); // Assume 9-6 workday

        Ok(CommandHistoryStats {
            total_commands,
            unique_commands,
            most_used_commands: most_used,
            typical_working_hours: typical_hours,
        })
    }

    async fn detect_prompt_style(&self) -> Result<String> {
        // Check shell config for prompt customization
        if let Some(config_path) = self.get_config_path() {
            if let Ok(content) = tokio::fs::read_to_string(&config_path).await {
                if content.contains("starship") {
                    return Ok("starship".to_string());
                } else if content.contains("powerlevel10k") || content.contains("p10k") {
                    return Ok("powerlevel10k".to_string());
                } else if content.contains("PS1=") && content.contains("\\u@\\h") {
                    return Ok("classic".to_string());
                } else if content.contains("oh-my-zsh") {
                    return Ok("oh-my-zsh".to_string());
                }
            }
        }
        
        Ok("default".to_string())
    }

    pub async fn extract_workflow_patterns(&self) -> Result<WorkflowPatterns> {
        let mut patterns = WorkflowPatterns::default();
        
        // Extract from git config
        if let Ok(output) = std::process::Command::new("git")
            .args(&["config", "--global", "--list"])
            .output() 
        {
            let config = String::from_utf8_lossy(&output.stdout);
            
            // Look for common branch naming patterns
            if config.contains("init.defaultbranch") {
                patterns.typical_branch_names.push("main".to_string());
                patterns.typical_branch_names.push("master".to_string());
            }
        }

        // Common patterns
        patterns.commit_message_patterns = vec![
            "feat: ".to_string(),
            "fix: ".to_string(),
            "docs: ".to_string(),
            "refactor: ".to_string(),
            "test: ".to_string(),
        ];

        patterns.code_review_checklist = vec![
            "Tests pass".to_string(),
            "Code follows style guide".to_string(),
            "Documentation updated".to_string(),
        ];

        patterns.testing_commands = vec![
            "cargo test".to_string(),
            "npm test".to_string(),
            "pytest".to_string(),
        ];

        Ok(patterns)
    }

    /// Get frequently used directories from history
    pub async fn get_frequent_directories(&self) -> Result<Vec<String>> {
        let mut dir_counts: HashMap<String, i64> = HashMap::new();
        
        if let Some(history_path) = self.get_history_path(&self.detect_shell()?) {
            if let Ok(content) = tokio::fs::read_to_string(&history_path).await {
                for line in content.lines() {
                    if line.contains("cd ") {
                        let parts: Vec<_> = line.split_whitespace().collect();
                        if parts.len() >= 2 {
                            let dir = parts[1..].join(" ");
                            *dir_counts.entry(dir).or_insert(0) += 1;
                        }
                    }
                }
            }
        }

        let mut dirs: Vec<_> = dir_counts.into_iter().collect();
        dirs.sort_by(|a, b| b.1.cmp(&a.1));
        
        Ok(dirs.into_iter().take(10).map(|(d, _)| d).collect())
    }
}