vlt 0.1.1

Fast offline-first CLI for managing .env files across environments
use std::io::{self, IsTerminal};
use std::path::Path;

use anyhow::{Context, Result, bail};
use dialoguer::{Select, theme::ColorfulTheme};

use crate::models::env_file::EnvFile;
use crate::models::rules::{VarRule, VltRules};
use crate::utils::output::{self, Icon};
use crate::utils::project;
use crate::utils::scanner;

pub const BASE_TEMPLATE_HEADER: &str = "# Do not modify this file directly.\n# This is the template vlt uses when creating environment files.";

pub fn run(apply: bool) -> Result<()> {
    let root = std::env::current_dir().context("failed to read current directory")?;
    project::ensure_initialized(&root)?;
    sync_discovered_vars(&root, apply)
}

pub fn sync_discovered_vars(root: &Path, apply: bool) -> Result<()> {
    sync_discovered_vars_with_output(root, apply, true)
}

pub fn sync_discovered_vars_quiet(root: &Path, apply: bool) -> Result<()> {
    sync_discovered_vars_with_output(root, apply, false)
}

fn sync_discovered_vars_with_output(
    root: &Path,
    apply: bool,
    show_write_paths: bool,
) -> Result<()> {
    let rules_path = root.join(".vlt/env.rules");
    let sample_path = root.join(".env.base");
    let mut rules = VltRules::load_or_default(&rules_path)?;
    let mut sample = EnvFile::load_or_default(&sample_path)?;
    let scan_result = scanner::scan_project(root)?;

    if scan_result.vars.is_empty() {
        output::print_line(Icon::Warning, "No environment variables found.");
        return Ok(());
    }

    output::print_line(
        Icon::Success,
        format!("Found {} environment variable(s).", scan_result.vars.len()),
    );

    let mut missing = Vec::new();
    for var in &scan_result.vars {
        let missing_sample = !sample.values.contains_key(var);
        let missing_rules = !rules.vars.contains_key(var);
        let needs_sync = missing_sample || missing_rules;
        let icon = if needs_sync { Icon::Warning } else { Icon::Success };
        let suffix = match (missing_sample, missing_rules) {
            (false, false) => String::new(),
            (true, true) => " (missing from .env.base and env.rules)".to_owned(),
            (true, false) => " (missing from .env.base)".to_owned(),
            (false, true) => " (missing from env.rules)".to_owned(),
        };
        output::print_line(icon, format!("{var}{suffix}"));
        if needs_sync {
            missing.push(var.clone());
        }
    }

    if missing.is_empty() {
        return Ok(());
    }

    let approved = choose_vars_to_add(&missing, apply)?;
    if approved.is_empty() {
        output::print_line(Icon::Info, "No new variables were added.");
        return Ok(());
    }

    let mut sample_changed = false;
    let mut rules_changed = false;
    for var in approved {
        sample_changed |= sample.insert_missing(&var);
        if !rules.vars.contains_key(&var) {
            rules.vars.insert(var.clone(), discovered_rule());
            rules_changed = true;
        }
    }

    if sample_changed {
        sample.save_with_header(&sample_path, Some(BASE_TEMPLATE_HEADER))?;
        if show_write_paths {
            output::print_line(Icon::Success, "Updated `.env.base`.");
        }
    }

    if rules_changed {
        rules.save(&rules_path)?;
        if show_write_paths {
            output::print_line(Icon::Success, "Updated `.vlt/env.rules`.");
        }
    }

    Ok(())
}

fn choose_vars_to_add(missing: &[String], apply: bool) -> Result<Vec<String>> {
    if apply {
        return Ok(missing.to_vec());
    }

    if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
        bail!(
            "interactive scan requires a terminal; rerun with --apply to add all discovered variables"
        );
    }

    let mut approved = Vec::new();
    let mut add_all = false;
    let theme = ColorfulTheme::default();

    for var in missing {
        if add_all {
            approved.push(var.clone());
            continue;
        }

        let options = ["Yes", "No", "Add all remaining"];
        let selection = Select::with_theme(&theme)
            .with_prompt(format!(
                "{} Add or sync {} to .env.base?",
                output::paint_icon(Icon::Info),
                &var
            ))
            .items(&options)
            .default(0)
            .interact()
            .context("failed to read scan choice")?;

        match selection {
            0 => approved.push(var.clone()),
            1 => {}
            2 => {
                approved.push(var.clone());
                add_all = true;
            }
            _ => unreachable!("select returned an unexpected option index"),
        }
    }

    Ok(approved)
}

fn discovered_rule() -> VarRule {
    VarRule::discovered()
}