solverforge-cli 2.0.1

CLI for scaffolding and managing SolverForge constraint solver projects
use dialoguer::{theme::ColorfulTheme, Select};
use owo_colors::OwoColorize;

use super::domain::DomainModel;
use super::skeleton::Pattern;

pub(crate) fn resolve_pattern_and_hardness(
    soft: bool,
    unary: bool,
    pair: bool,
    join: bool,
    balance: bool,
    reward: bool,
    domain: &DomainModel,
) -> Result<(Pattern, bool), String> {
    let explicit_pattern: Option<Pattern> = match (unary, pair, join, balance, reward) {
        (true, _, _, _, _) => Some(Pattern::Unary),
        (_, true, _, _, _) => Some(Pattern::Pair),
        (_, _, true, _, _) => Some(Pattern::Join),
        (_, _, _, true, _) => Some(Pattern::Balance),
        (_, _, _, _, true) => Some(Pattern::Reward),
        _ => None,
    };

    // balance and reward imply soft
    let pattern_implies_soft = matches!(
        explicit_pattern,
        Some(Pattern::Balance) | Some(Pattern::Reward)
    );
    let is_soft_explicit = soft || pattern_implies_soft;

    match explicit_pattern {
        Some(p) => Ok((p, is_soft_explicit)),
        None => {
            // Interactive wizard
            run_wizard(soft, domain)
        }
    }
}

fn run_wizard(soft_flag: bool, domain: &DomainModel) -> Result<(Pattern, bool), String> {
    println!("{} Scanning domain model...", "".bright_green());
    println!();
    println!("  Found: {}", domain.solution_type.bright_white().bold());
    for e in &domain.entities {
        let mut solvable_fields = Vec::new();
        solvable_fields.extend(
            e.scalar_vars
                .iter()
                .map(|field| format!("{} [scalar]", field.field)),
        );
        solvable_fields.extend(
            e.list_vars
                .iter()
                .map(|field| format!("{} [list]", field.field)),
        );
        let var_info = if solvable_fields.is_empty() {
            String::new()
        } else {
            format!("  — solvable fields: {}", solvable_fields.join(", "))
        };
        println!(
            "    Entities:  {} ({}){}",
            e.field_name.bright_cyan(),
            e.item_type,
            var_info
        );
    }
    for f in &domain.facts {
        println!(
            "    Facts:     {} ({})",
            f.field_name.bright_cyan(),
            f.item_type
        );
    }
    println!();

    // Pattern selection
    let has_join = !domain.facts.is_empty();

    let mut pattern_options: Vec<(&str, Pattern)> = vec![
        (
            "Penalize matching entities        (e.g. unassigned, invalid state)",
            Pattern::Unary,
        ),
        (
            "Penalize conflicting pairs        (e.g. overlapping shifts, double-booking)",
            Pattern::Pair,
        ),
    ];
    if has_join {
        pattern_options.push((
            "Penalize entity-fact mismatch     (e.g. missing skill, wrong location)",
            Pattern::Join,
        ));
    }
    pattern_options.push((
        "Balance assignments                (e.g. fair workload distribution)",
        Pattern::Balance,
    ));
    pattern_options.push((
        "Reward matching entities           (e.g. desired day, preferred shift)",
        Pattern::Reward,
    ));

    let labels: Vec<&str> = pattern_options.iter().map(|(l, _)| *l).collect();

    let pattern_idx = Select::with_theme(&ColorfulTheme::default())
        .with_prompt("Constraint type")
        .items(&labels)
        .default(0)
        .interact()
        .map_err(|e| format!("prompt error: {}", e))?;

    let pattern = pattern_options[pattern_idx].1;

    // Hard/soft selection (skip if pattern implies soft)
    let is_soft = if matches!(pattern, Pattern::Balance | Pattern::Reward) || soft_flag {
        true
    } else {
        let hardness_idx = Select::with_theme(&ColorfulTheme::default())
            .with_prompt("Hard or soft")
            .items([
                "Hard  (must be satisfied — correctness)",
                "Soft  (should be optimized — quality)",
            ])
            .default(0)
            .interact()
            .map_err(|e| format!("prompt error: {}", e))?;
        hardness_idx == 1
    };

    Ok((pattern, is_soft))
}