forklaunch 1.15.0

Launch faster with forklaunch
use std::{collections::HashMap, io::Write, path::Path};

use anyhow::{Result, bail};
use clap::ArgMatches;
use rustyline::{Editor, history::DefaultHistory};
use serde_json::from_str as json_from_str;
use termcolor::{StandardStream, WriteColor};

use super::detection::{DetectedConfig, detect_description_from_package_json};
use crate::{
    constants::{Database, Infrastructure, WorkerType},
    core::package_json::project_package_json::ProjectPackageJson,
    prompt::{
        ArrayCompleter, prompt_comma_separated_list_with_answers,
        prompt_with_validation_with_answers, prompt_without_validation_with_answers,
    },
};

pub fn resolve_database_config(
    project_name: &str,
    project_path: &Path,
    detected: &DetectedConfig,
    matches: &ArgMatches,
    prompts_map: &HashMap<String, HashMap<String, String>>,
    stdout: &mut StandardStream,
) -> Result<Database> {
    let override_value = prompts_map
        .get(project_name)
        .and_then(|p| p.get("database"))
        .map(|s| s.as_str());

    if let Some(detected_db) = &detected.database {
        if override_value.is_none() {
            return Ok(*detected_db);
        }
    }

    if let Some(override_str) = override_value {
        if override_str == "none" {
            return fallback_to_package_json(project_path, stdout);
        } else {
            return Ok(override_str.parse()?);
        }
    }

    log_warn!(stdout, "Could not detect database configuration. Please specify:");

    let mut line_editor = Editor::<ArrayCompleter, DefaultHistory>::new()?;
    let mut database_options = vec!["none"];
    database_options.extend_from_slice(&Database::VARIANTS);

    let database_input = prompt_with_validation_with_answers(
        &mut line_editor,
        stdout,
        "database",
        matches,
        "database type",
        Some(&database_options),
        |input| database_options.contains(&input),
        |_| "Invalid database type. Please try again".to_string(),
        project_name,
        prompts_map,
    )?;

    if database_input == "none" {
        fallback_to_package_json(project_path, stdout)
    } else {
        Ok(database_input.parse()?)
    }
}

pub fn resolve_infrastructure_config(
    project_name: &str,
    detected: &DetectedConfig,
    matches: &ArgMatches,
    prompts_map: &HashMap<String, HashMap<String, String>>,
    _stdout: &mut StandardStream,
) -> Result<Vec<Infrastructure>> {
    let override_value = prompts_map
        .get(project_name)
        .and_then(|p| p.get("infrastructure"));

    if let Some(override_str) = override_value {
        if override_str == "none" {
            return Ok(vec![]);
        } else {
            return Ok(override_str
                .split(',')
                .map(|s| s.trim().to_string())
                .filter(|s| !s.is_empty())
                .map(|s| s.parse().unwrap())
                .collect());
        }
    }

    if !detected.infrastructure.is_empty() {
        return Ok(detected.infrastructure.clone());
    }

    let mut line_editor = Editor::<ArrayCompleter, DefaultHistory>::new()?;
    Ok(prompt_comma_separated_list_with_answers(
        &mut line_editor,
        "infrastructure",
        matches,
        &Infrastructure::VARIANTS,
        None,
        "additional infrastructure components",
        true,
        project_name,
        prompts_map,
    )?
    .iter()
    .map(|s| s.parse().unwrap())
    .collect())
}

pub fn resolve_description(
    project_name: &str,
    project_path: &Path,
    matches: &ArgMatches,
    prompts_map: &HashMap<String, HashMap<String, String>>,
    stdout: &mut StandardStream,
) -> Result<String> {
    let detected_description = detect_description_from_package_json(project_path)
        .ok()
        .flatten();

    if let Some(detected) = &detected_description {
        let override_value = prompts_map
            .get(project_name)
            .and_then(|p| p.get("description"));

        if override_value.is_none() {
            log_ok!(stdout, "Detected description: {}", detected);

            let mut line_editor = Editor::<ArrayCompleter, DefaultHistory>::new()?;
            let override_choice = prompt_with_validation_with_answers(
                &mut line_editor,
                stdout,
                "override_description",
                matches,
                "Override detected description? (y/n)",
                Some(&["y", "n", "yes", "no"]),
                |input| matches!(input, "y" | "n" | "yes" | "no"),
                |_| "Please enter 'y' or 'n'".to_string(),
                project_name,
                prompts_map,
            )?;

            if matches!(override_choice.as_str(), "n" | "no") {
                return Ok(detected.clone());
            }
        } else {
            return Ok(override_value.unwrap().clone());
        }
    }

    let mut line_editor = Editor::<ArrayCompleter, DefaultHistory>::new()?;
    prompt_without_validation_with_answers(
        &mut line_editor,
        stdout,
        "description",
        matches,
        "project description",
        None,
        project_name,
        prompts_map,
    )
}

pub fn fallback_to_package_json(
    project_path: &Path,
    stdout: &mut StandardStream,
) -> Result<Database> {
    let package_json_path = project_path.join("package.json");

    if !package_json_path.exists() {
        log_error!(stdout, "package.json not found");
        bail!("Cannot determine database configuration: package.json not found");
    }

    let content = std::fs::read_to_string(&package_json_path)?;
    let package_json: ProjectPackageJson = json_from_str(&content)?;

    if let Some(deps) = &package_json.dependencies {
        let found_db = deps
            .databases
            .iter()
            .find(|db| Database::VARIANTS.contains(&db.to_string().as_str()));

        if let Some(db) = found_db {
            return Ok(*db);
        }
    }

    bail!("No database found in package.json dependencies");
}

pub fn display_detection_results(
    detected: &DetectedConfig,
    stdout: &mut StandardStream,
) -> Result<()> {
    if let Some(ref db) = detected.database {
        log_ok!(stdout, "Detected database: {}", db.to_string());
    }

    if !detected.infrastructure.is_empty() {
        log_ok!(stdout, "Detected infrastructure: {}", detected.infrastructure.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(", "));
    }

    if let Some(ref desc) = detected.description {
        log_ok!(stdout, "Detected description: {}", desc);
    }

    if detected.database.is_none()
        && detected.infrastructure.is_empty()
        && detected.description.is_none()
        && detected.worker_type.is_none()
    {
        log_warn!(stdout, "No configuration auto-detected");
    }

    Ok(())
}

pub fn resolve_worker_type(
    worker_name: &str,
    detected: &DetectedConfig,
    matches: &ArgMatches,
    prompts_map: &HashMap<String, HashMap<String, String>>,
    stdout: &mut StandardStream,
) -> Result<WorkerType> {
    if let Some(project_prompts) = prompts_map.get(worker_name) {
        if let Some(worker_type_str) = project_prompts.get("type") {
            return Ok(worker_type_str.parse()?);
        }
    }

    if let Some(detected_type) = &detected.worker_type {
        log_ok!(stdout, "Detected worker type: {}", detected_type.to_string());

        let mut line_editor = Editor::<ArrayCompleter, DefaultHistory>::new()?;
        let override_choice = prompt_with_validation_with_answers(
            &mut line_editor,
            stdout,
            "override_worker_type",
            matches,
            "Override detected worker type? (y/n)",
            Some(&["y", "n", "yes", "no"]),
            |input| matches!(input, "y" | "n" | "yes" | "no"),
            |_| "Please enter 'y' or 'n'".to_string(),
            worker_name,
            prompts_map,
        )?;

        if matches!(override_choice.as_str(), "n" | "no") {
            return Ok(*detected_type);
        }
    }

    let mut line_editor = Editor::<ArrayCompleter, DefaultHistory>::new()?;
    let worker_type_str = prompt_with_validation_with_answers(
        &mut line_editor,
        stdout,
        "type",
        matches,
        "worker type",
        Some(&WorkerType::VARIANTS),
        |input| WorkerType::VARIANTS.contains(&input),
        |_| "Invalid worker type. Please try again".to_string(),
        worker_name,
        prompts_map,
    )?;

    Ok(worker_type_str.parse()?)
}