yoo 0.6.3

A tiny developer companion for better coding sessions.
use std::{error::Error, io};

#[cfg(not(windows))]
use std::io::IsTerminal;

use crate::{
    args::{self, Command, FetchOptions, ProjectOptions, RunOptions, SessionOptions},
    config::{self, WriteResult},
    content, doctor, fetch, git, project, timer, tips,
    ui::{Theme, Ui},
};

pub fn execute(command: Command) -> Result<(), Box<dyn Error>> {
    match command {
        Command::Run(options) => run(options),
        Command::Init => init(),
        Command::ConfigPath => {
            println!("{}", config::config_path().display());
            Ok(())
        }
        Command::Doctor => {
            let directory = std::env::current_dir()?;
            doctor::print(&doctor::collect(&directory));
            Ok(())
        }
        Command::Fetch(options) => run_fetch(options),
        Command::Project(options) => run_project(options),
        Command::Session(options) => session(options),
        Command::Tip(pack) => print_tip(pack),
        Command::Tips => list_tips(),
        Command::Help => {
            print!("{}", args::help_text());
            Ok(())
        }
        Command::Version => {
            println!("yoo {}", env!("CARGO_PKG_VERSION"));
            Ok(())
        }
    }
}

fn init() -> Result<(), Box<dyn Error>> {
    let config_path = config::config_path();
    let tip_path = config::tip_packs_dir().join("community.yaml");

    match config::write_default()? {
        WriteResult::Created => println!("Created {}", config_path.display()),
        WriteResult::AlreadyExists => {
            println!("Config already exists at {}", config_path.display())
        }
    }

    match tips::write_sample_pack()? {
        WriteResult::Created => println!("Created sample tip pack {}", tip_path.display()),
        WriteResult::AlreadyExists => {
            println!("Sample tip pack already exists at {}", tip_path.display())
        }
    }

    Ok(())
}

fn run(options: RunOptions) -> Result<(), Box<dyn Error>> {
    let config = config::load()?;
    let ui = ui_for_run(&config, &options)?;

    let name = options
        .name
        .as_deref()
        .unwrap_or(&config.profile.name)
        .trim();

    if config.appearance.ascii && !options.no_art {
        ui.print_art()?;
        ui.blank_line()?;
    }

    ui.heading("yoo — developer session starter")?;
    ui.type_line(&content::greeting(name))?;

    if config.hydration.enabled {
        ui.info("💧", "Hydration:", &content::hydration_reminder())?;
    }

    ui.blank_line()?;

    let directory = std::env::current_dir()?;
    let project_name = directory
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("current directory");

    ui.divider()?;
    ui.info("📁", "Project:", project_name)?;

    if config.git.show_branch {
        if let Some(info) = git::inspect(&directory) {
            ui.info("🌿", "Git branch:", &info.branch)?;

            if config.git.show_status {
                ui.info(
                    "✏️",
                    "Working tree:",
                    &git::change_status(info.changed_files),
                )?;
            }
        } else {
            ui.info("🌿", "Git:", "not a repository")?;
        }
    }

    if config.tips.enabled {
        ui.divider()?;
        ui.info("💡", "Tip:", &tips::random_tip(&config.tips.pack)?)?;
    }

    ui.type_line("Go build something fun. 👋")?;
    Ok(())
}

fn run_fetch(options: FetchOptions) -> Result<(), Box<dyn Error>> {
    let directory = std::env::current_dir()?;
    let report = fetch::collect(&directory);

    if options.json {
        println!("{}", fetch::to_json(&report)?);
        return Ok(());
    }

    let config = config::load()?;
    let ui = ui_for_fetch(&config, &options)?;

    if config.appearance.ascii && !options.no_art {
        ui.print_art()?;
        ui.blank_line()?;
    }

    fetch::print(&report, &ui)?;
    Ok(())
}

fn run_project(options: ProjectOptions) -> Result<(), Box<dyn Error>> {
    let directory = std::env::current_dir()?;
    let report = project::collect(&directory);

    if options.json {
        println!("{}", project::to_json(&report)?);
        return Ok(());
    }

    let config = config::load()?;
    let ui = ui_for_project(&config, &options)?;

    if config.appearance.ascii && !options.no_art {
        ui.print_art()?;
        ui.blank_line()?;
    }

    project::print(&report, &ui)?;
    Ok(())
}

fn session(options: SessionOptions) -> Result<(), Box<dyn Error>> {
    let config = config::load()?;
    let minutes = options.minutes.unwrap_or(config.session.default_minutes);
    timer::validate_minutes(minutes)
        .map_err(|message| io::Error::new(io::ErrorKind::InvalidInput, message))?;

    timer::start(minutes, config.session.show_complete_message)?;
    Ok(())
}

fn print_tip(pack: Option<String>) -> Result<(), Box<dyn Error>> {
    let config = config::load()?;
    let pack = pack.unwrap_or(config.tips.pack);
    println!("💡 {}", tips::random_tip(&pack)?);
    Ok(())
}

fn list_tips() -> Result<(), Box<dyn Error>> {
    println!("Available yoo tip packs:\n");

    for pack in tips::list_packs()? {
        let source = match pack.source {
            tips::PackSource::BuiltIn => "built-in",
            tips::PackSource::Local => "local",
        };
        println!("- {:<12} [{}] {}", pack.name, source, pack.description);
    }

    println!("\nAdd community packs as YAML files in:");
    println!("{}", config::tip_packs_dir().display());
    Ok(())
}

fn ui_for_run(config: &config::Config, options: &RunOptions) -> Result<Ui, io::Error> {
    ui_for_display(
        config,
        options.theme.as_deref(),
        options.plain,
        if options.fast {
            0
        } else {
            config.appearance.typing_speed_ms
        },
    )
}

fn ui_for_fetch(config: &config::Config, options: &FetchOptions) -> Result<Ui, io::Error> {
    ui_for_display(config, options.theme.as_deref(), options.plain, 0)
}

fn ui_for_project(config: &config::Config, options: &ProjectOptions) -> Result<Ui, io::Error> {
    ui_for_display(config, options.theme.as_deref(), options.plain, 0)
}

fn ui_for_display(
    config: &config::Config,
    theme_override: Option<&str>,
    plain: bool,
    typing_speed_ms: u64,
) -> Result<Ui, io::Error> {
    let theme_name = theme_override.unwrap_or(&config.appearance.theme);
    let theme = Theme::parse(theme_name).ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidInput,
            format!(
                "unknown theme `{theme_name}`; use one of: {}",
                Theme::names().join(", ")
            ),
        )
    })?;

    let interactive = stdout_is_interactive();

    Ok(Ui::new(
        theme,
        config.appearance.colors && !plain && interactive,
        if interactive { typing_speed_ms } else { 0 },
    ))
}

#[cfg(not(windows))]
fn stdout_is_interactive() -> bool {
    io::stdout().is_terminal()
}

#[cfg(windows)]
fn stdout_is_interactive() -> bool {
    use std::ffi::c_void;

    const STD_OUTPUT_HANDLE: u32 = -11_i32 as u32;
    const INVALID_HANDLE_VALUE: *mut c_void = -1_isize as *mut c_void;

    #[link(name = "kernel32")]
    unsafe extern "system" {
        fn GetStdHandle(std_handle: u32) -> *mut c_void;
        fn GetConsoleMode(console: *mut c_void, mode: *mut u32) -> i32;
    }

    let handle = unsafe { GetStdHandle(STD_OUTPUT_HANDLE) };
    if handle.is_null() || handle == INVALID_HANDLE_VALUE {
        return false;
    }

    let mut mode = 0;
    unsafe { GetConsoleMode(handle, &mut mode) != 0 }
}