sara-tasks 1.1.2

Sara — folder-aware task manager
use anyhow::Result;
use rusqlite::Connection;
use std::io::{self, Write};

use crate::infrastructure::config::Config;
use crate::infrastructure::model::Project;
use crate::infrastructure::project::find_git_root;

/// Detect which tech stacks are present in the project root.
pub fn detect_stack(path: &str) -> String {
    let root = std::path::Path::new(path);
    let mut stacks = vec![];
    let markers = [
        ("Cargo.toml", "Rust"),
        ("package.json", "Node.js/JS"),
        ("pyproject.toml", "Python"),
        ("requirements.txt", "Python"),
        ("go.mod", "Go"),
        ("pom.xml", "Java/Maven"),
        ("build.gradle", "Java/Gradle"),
        ("Gemfile", "Ruby"),
        ("composer.json", "PHP"),
        ("pubspec.yaml", "Dart/Flutter"),
        ("*.swift", "Swift"),
        ("CMakeLists.txt", "C/C++"),
        ("mix.exs", "Elixir"),
    ];
    for (file, label) in &markers {
        if file.contains('*') {
            // glob-ish: skip for simplicity, just check extension presence
        } else if root.join(file).exists() {
            stacks.push(*label);
        }
    }
    // Check for Swift files manually
    if let Ok(rd) = std::fs::read_dir(root) {
        for entry in rd.flatten() {
            if entry
                .path()
                .extension()
                .and_then(|e| e.to_str())
                .map(|e| e == "swift")
                .unwrap_or(false)
                && !stacks.contains(&"Swift")
            {
                stacks.push("Swift");
            }
        }
    }
    if stacks.is_empty() {
        "unknown".to_string()
    } else {
        stacks.join(", ")
    }
}

fn prompt(msg: &str, default: Option<&str>) -> Result<String> {
    let prompt_str = if let Some(d) = default {
        format!("{msg} [{d}]: ")
    } else {
        format!("{msg}: ")
    };
    print!("{prompt_str}");
    io::stdout().flush()?;
    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    let trimmed = input.trim().to_string();
    if trimmed.is_empty() {
        Ok(default.unwrap_or("").to_string())
    } else {
        Ok(trimmed)
    }
}

#[allow(clippy::too_many_arguments)]
pub fn run(
    conn: &Connection,
    cfg: &Config,
    name_override: Option<&str>,
    goal_override: Option<&str>,
    stack_override: Option<&str>,
    conventions_override: Option<&str>,
    notes_override: Option<&str>,
    setup_cmd: Option<&str>,
    test_cmd: Option<&str>,
    lint_cmd: Option<&str>,
    run_cmd: Option<&str>,
    yes: bool,
) -> Result<()> {
    let cwd = std::env::current_dir()?;
    let git_root = find_git_root(&cwd);

    // Resolve the project from the current folder: a git repo is its own
    // project; otherwise the folder itself is initialized as the project.
    let (resolved_name, resolved_path) =
        crate::infrastructure::project::project_identity_for_dir(&cwd, cfg);
    let project_name = name_override.map(str::to_string).unwrap_or(resolved_name);
    let project_path = Some(resolved_path);

    if git_root.is_none() {
        println!(
            "Note: not inside a git repo — initializing the current folder as project '{}'.",
            project_name
        );
    }

    // Detect stack (auto-detection can be overridden via --stack)
    let detected_stack = project_path
        .as_deref()
        .map(detect_stack)
        .unwrap_or_else(|| "unknown".to_string());

    println!("Initializing project: {}", project_name);
    println!("Detected stack: {}", detected_stack);

    // Load existing profile if any
    let existing = crate::infrastructure::db::get_project(conn, &project_name)?;

    // Stack: explicit --stack wins, else preserve an existing value, else use detection.
    let resolved_stack = stack_override
        .map(str::to_string)
        .or_else(|| existing.as_ref().and_then(|p| p.stack.clone()))
        .unwrap_or_else(|| detected_stack.clone());

    let goal = if let Some(g) = goal_override {
        g.to_string()
    } else if yes {
        existing
            .as_ref()
            .and_then(|p| p.goal.clone())
            .unwrap_or_default()
    } else {
        let current_goal = existing.as_ref().and_then(|p| p.goal.as_deref());
        prompt("What is this project? (one-line goal)", current_goal)?
    };

    let notes = if let Some(n) = notes_override {
        n.to_string()
    } else if yes {
        existing
            .as_ref()
            .and_then(|p| p.notes.clone())
            .unwrap_or_default()
    } else {
        let current_notes = existing.as_ref().and_then(|p| p.notes.as_deref());
        prompt("Any conventions or notes? (optional)", current_notes)?
    };

    // Conventions: set via flag, otherwise preserve any existing value.
    let conventions = conventions_override
        .map(str::to_string)
        .or_else(|| existing.as_ref().and_then(|p| p.conventions.clone()));

    let project = Project {
        name: project_name.clone(),
        path: project_path,
        goal: if goal.is_empty() {
            None
        } else {
            Some(goal.clone())
        },
        stack: Some(resolved_stack.clone()),
        conventions,
        notes: if notes.is_empty() { None } else { Some(notes) },
        initialized_at: Some(chrono::Utc::now()),
        last_seen: Some(chrono::Utc::now()),
        github_repo: None,
        github_login: None,
        github_sync_scope: None,
    };

    crate::infrastructure::db::save_project_profile(conn, &project)?;

    if setup_cmd.is_some() || test_cmd.is_some() || lint_cmd.is_some() || run_cmd.is_some() {
        crate::infrastructure::db::set_project_commands(
            conn,
            &project_name,
            &crate::infrastructure::db::ProjectCommands {
                setup_cmd: setup_cmd.map(str::to_string),
                test_cmd: test_cmd.map(str::to_string),
                lint_cmd: lint_cmd.map(str::to_string),
                run_cmd: run_cmd.map(str::to_string),
            },
        )?;
    }

    println!("✔ Project '{}' profile saved.", project_name);

    if let Some(g) = &project.goal {
        println!("  Goal:  {g}");
    }
    println!("  Stack: {resolved_stack}");

    let commands = crate::infrastructure::db::get_project_commands(conn, &project_name)?;
    for (label, cmd) in [
        ("Setup", &commands.setup_cmd),
        ("Test", &commands.test_cmd),
        ("Lint", &commands.lint_cmd),
        ("Run", &commands.run_cmd),
    ] {
        if let Some(c) = cmd {
            println!("  {label}:  {c}");
        }
    }

    Ok(())
}