flesh 0.0.12

Flora's LowRes Extensible Super Highway
use {
    anyhow::Context,
    clap::{Parser, Subcommand},
    indicatif::{ProgressBar, ProgressStyle},
    serde::{Deserialize, Serialize},
    std::{
        fs::{OpenOptions, create_dir_all},
        io::{Read, Write},
        path::{Path, PathBuf},
        time::Duration,
    },
    tokio::{fs::remove_dir_all, process::Command as TokioCommand, task},
};

fn config_loc() -> PathBuf {
    let path = Path::new(&std::env::var("HOME").expect("You don't seem to have a homedir"))
        .join(".config")
        .join("flesh")
        .to_path_buf();

    if !path.exists() {
        let _ = create_dir_all(&path);
    }

    path
}

fn config_file() -> anyhow::Result<FleshConfig> {
    let config_path = config_loc().join("config.toml");

    let mut oo = OpenOptions::new()
        .create(true)
        .truncate(true)
        .read(true)
        .write(true)
        .open(&config_path)
        .context(format!("Failed to open or create config file at {:?}", &config_path))?;

    let mut string = String::new();
    oo.read_to_string(&mut string)?;

    if string.is_empty() {
        let default_config = FleshConfig::default();
        let default_toml = toml::to_string_pretty(&default_config)?;

        oo.set_len(0)?;
        oo.write_all(default_toml.as_bytes())?;

        Ok(default_config)
    } else {
        toml::from_str(&string).context("Failed to parse config file as TOML")
    }
}

#[derive(Parser, Debug)]
struct FleshManagerArgs {
    #[command(subcommand)]
    command: SubCommand,
}

#[derive(Debug, Subcommand)]
enum SubCommand {
    List,
    Add { github_url: String, subdomain: String },
    Remove { subdomain: String },
    Run,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FleshApp {
    pub subdomain: String,
    pub module: String,
}

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct FleshConfig {
    pub apps: Vec<FleshApp>,
}

// Normalizes a GitHub URL (e.g., "https://github.com/user/repo.git") into a safe folder name (e.g., "user-repo")
fn normalize_github_url(url: &str) -> String {
    url.trim_end_matches(".git")
        .trim_start_matches("https://")
        .trim_start_matches("http://")
        .trim_start_matches("github.com/")
        .replace('/', "-")
}

#[tokio::main]

async fn main() -> anyhow::Result<()> {
    let args = FleshManagerArgs::parse();
    let mut config = config_file()?;

    match args.command {
        SubCommand::List => {
            println!("{:#?}", config);
        }
        SubCommand::Add { github_url, subdomain } => {
            if config.apps.iter().any(|app| app.subdomain == subdomain) {
                anyhow::bail!("App with subdomain '{}' already exists.", subdomain);
            }

            let normalized_name = normalize_github_url(&github_url);
            let app_dir = config_loc().join(&normalized_name);

            if app_dir.exists() {
                anyhow::bail!("Application folder for URL '{}' already exists at {:?}", github_url, app_dir);
            }

            let spinner = ProgressBar::new_spinner();
            spinner.set_style(
                ProgressStyle::with_template("{spinner:.green} {msg}")
                    .unwrap()
                    .tick_strings(&["", "", "", "", "", "", "", "", "", ""]),
            );
            spinner.set_message(format!("Cloning '{}' to {:?}...", github_url, app_dir));
            spinner.enable_steady_tick(Duration::from_millis(100));

            let clone_result = task::spawn({
                let app_dir = app_dir.clone();
                async move { TokioCommand::new("git").arg("clone").arg(&github_url).arg(app_dir).output().await }
            })
            .await
            .context("Task execution failed")?;

            let output = clone_result.context("Failed to execute git clone. Is git installed and in your PATH?")?;

            spinner.finish_and_clear();

            if !output.status.success() {
                if app_dir.clone().exists() {
                    let _ = remove_dir_all(&app_dir).await;
                }
                anyhow::bail!("Git clone failed:\n{}", String::from_utf8_lossy(&output.stderr));
            }

            println!("✅ Clone successful.");

            // TODO: Cargo build --release, ensure cdylib exists

            let new_app = FleshApp { subdomain, module: normalized_name };
            config.apps.push(new_app.clone());
            println!("Added app: {:#?}", new_app);
        }
        SubCommand::Remove { subdomain } => {
            let initial_len = config.apps.len();
            let app_to_remove = config.apps.iter().find(|app| app.subdomain == subdomain).cloned();

            if let Some(app) = app_to_remove {
                config.apps.retain(|app| app.subdomain != subdomain);

                if config.apps.len() == initial_len {
                    anyhow::bail!("Failed to remove app from config vector.");
                }

                println!("Removed app '{}' from configuration.", subdomain);

                let app_dir = config_loc().join(&app.module);

                if app_dir.exists() {
                    let spinner = ProgressBar::new_spinner();
                    spinner.set_style(
                        ProgressStyle::with_template("{spinner:.yellow} {msg}").unwrap().tick_strings(&["", "", "", ""]),
                    );
                    spinner.set_message(format!("Removing application folder at {:?}...", app_dir));
                    spinner.enable_steady_tick(Duration::from_millis(150));

                    remove_dir_all(&app_dir)
                        .await
                        .with_context(|| format!("Failed to remove application folder at {:?}", app_dir))?;

                    spinner.finish_and_clear();
                    println!("🗑️ Folder removed.");
                } else {
                    println!("Warning: Application folder {:?} not found. Configuration updated.", app_dir);
                }
            } else {
                anyhow::bail!("App with subdomain '{}' not found in config.", subdomain);
            }
        }
        SubCommand::Run => {}
    }

    let save_spinner = ProgressBar::new_spinner();
    save_spinner.set_style(
        ProgressStyle::with_template("{spinner:.blue} {msg}")
            .unwrap()
            .tick_strings(&["", "", "", "", "", "", "", "", "", "", "", "", "", ""]),
    );
    save_spinner.set_message("Saving configuration...");
    save_spinner.enable_steady_tick(Duration::from_millis(80));

    let config_path = config_loc().join("config.toml");
    let mut oo = OpenOptions::new()
        .write(true)
        .truncate(true)
        .open(&config_path)
        .context(format!("Failed to open config file for writing at {:?}", &config_path))?;

    let toml_string = toml::to_string_pretty(&config)?;
    oo.write_all(toml_string.as_bytes())?;

    save_spinner.finish_and_clear();
    println!("🎉 Configuration saved successfully.");

    Ok(())
}