runmat 0.5.4

High-performance MATLAB/Octave syntax mathematical runtime
use anyhow::{Context, Result};
use runmat_config::runtime::RunMatRuntimeConfig;
use runmat_runtime::analysis::{
    analysis_plan_study_op, analysis_plan_study_sweep_op, analysis_validate_study_op,
    analysis_validate_study_sweep_op, is_fea_file_path, load_fea_document_from_path_async,
    AnalysisStudyPlanData, AnalysisStudySweepPlanData, AnalysisStudySweepValidateData,
    AnalysisStudyValidateResult, FeaResolvedDocument,
};
use runmat_runtime::operations::{OperationContext, OperationErrorEnvelope};
use serde::Serialize;
use serde_json::json;
use std::path::{Path, PathBuf};

use crate::cli::Cli;
use crate::commands::bytecode::emit_bytecode;
use crate::commands::script::resolve_script_input;
use crate::AlreadyReportedCliError;

pub async fn execute_check(
    file: PathBuf,
    _cli: &Cli,
    config: &RunMatRuntimeConfig,
    json: bool,
) -> Result<()> {
    let file = resolve_script_input(file)?;
    if is_fea_file_path(&file) {
        return check_fea_file(file, config, json).await;
    }
    if is_matlab_file(&file) {
        return check_m_file(file, config, json).await;
    }

    anyhow::bail!(
        "runmat check supports .m scripts and .fea documents; got {}",
        file.display()
    )
}

async fn check_fea_file(path: PathBuf, config: &RunMatRuntimeConfig, json: bool) -> Result<()> {
    if !json {
        eprintln!("Checking {}", path.display());
        eprintln!("  loading study document");
    }
    let document = load_fea_document_from_path_async(&path)
        .await
        .map_err(|err| anyhow::anyhow!("Failed to load FEA file {}: {err}", path.display()))?;
    let context = OperationContext::new(None, None);
    match document {
        FeaResolvedDocument::Study(spec) => {
            if !json {
                eprintln!(
                    "  study: {} ({:?}, {:?})",
                    spec.study_id, spec.run_kind, spec.backend
                );
                eprintln!(
                    "  geometry: {} regions, {} meshes",
                    spec.geometry.regions.len(),
                    spec.geometry.meshes.len()
                );
                eprintln!("  validating");
            }
            let validation = analysis_validate_study_op(&spec, context.clone())
                .map_err(report_operation_error)?;
            if !validation.data.valid {
                if json {
                    print_payload("study", "invalid", &validation.data, config.runtime.verbose)?;
                } else {
                    print_study_validation_failure(&validation.data);
                }
                return Err(AlreadyReportedCliError.into());
            }
            if !json {
                eprintln!("  planning");
            }
            let plan = analysis_plan_study_op(&spec, context).map_err(report_operation_error)?;
            if json {
                print_payload(
                    "study",
                    "valid",
                    &json!({
                        "validation": validation.data,
                        "plan": plan.data,
                    }),
                    config.runtime.verbose,
                )?;
            } else {
                print_study_check_summary(&validation.data, &plan.data, config.runtime.verbose);
            }
        }
        FeaResolvedDocument::Sweep(spec) => {
            if !json {
                eprintln!(
                    "  sweep: {} ({} studies, fail_fast: {})",
                    spec.sweep_id,
                    spec.studies.len(),
                    spec.fail_fast
                );
                eprintln!("  validating");
            }
            let validation = analysis_validate_study_sweep_op(&spec, context.clone())
                .map_err(report_operation_error)?;
            if !validation.data.valid {
                if json {
                    print_payload("sweep", "invalid", &validation.data, config.runtime.verbose)?;
                } else {
                    print_sweep_validation_failure(&validation.data);
                }
                return Err(AlreadyReportedCliError.into());
            }
            if !json {
                eprintln!("  planning");
            }
            let plan =
                analysis_plan_study_sweep_op(&spec, context).map_err(report_operation_error)?;
            if json {
                print_payload(
                    "sweep",
                    "valid",
                    &json!({
                        "validation": validation.data,
                        "plan": plan.data,
                    }),
                    config.runtime.verbose,
                )?;
            } else {
                print_sweep_check_summary(&validation.data, &plan.data);
            }
        }
    }
    Ok(())
}

async fn check_m_file(path: PathBuf, config: &RunMatRuntimeConfig, json: bool) -> Result<()> {
    let content = runmat_filesystem::read_to_string_async(&path)
        .await
        .with_context(|| format!("Failed to read script file: {}", path.display()))?;
    emit_bytecode(&content, config, Some(path.to_string_lossy().as_ref()))
        .with_context(|| format!("Failed to check {}", path.display()))?;
    if json {
        println!(
            "{}",
            serde_json::to_string_pretty(&json!({
                "document_kind": "script",
                "status": "valid",
                "path": path,
            }))
            .context("Failed to serialize check result")?
        );
    } else {
        println!("OK {}", path.display());
    }
    Ok(())
}

fn print_study_check_summary(
    validation: &AnalysisStudyValidateResult,
    plan: &AnalysisStudyPlanData,
    verbose: bool,
) {
    println!("OK {}", plan.study_id);
    println!("  kind: {:?}", plan.run_kind);
    println!("  backend: {:?}", plan.backend);
    println!("  model: {}", plan.model_id);
    println!("  validation: passed ({} issues)", validation.issues.len());
    println!("  run op: {} ({})", plan.run_operation, plan.run_op_version);
    println!("  evidence: {}", plan.evidence_artifact_path);
    if verbose {
        println!("  study fingerprint: {}", plan.study_fingerprint);
        println!("  operations:");
        for operation in &plan.operation_sequence {
            println!("    {operation}");
        }
    }
}

fn print_study_validation_failure(validation: &AnalysisStudyValidateResult) {
    println!("FAILED validation");
    println!("  evidence: {}", validation.evidence_artifact_path);
    for issue in &validation.issues {
        println!("  {}: {}", issue.code, issue.message);
    }
}

fn print_sweep_check_summary(
    validation: &AnalysisStudySweepValidateData,
    plan: &AnalysisStudySweepPlanData,
) {
    println!("OK {}", plan.sweep_id);
    println!("  studies: {}", plan.study_count);
    println!("  planned: {}", plan.planned_count);
    println!("  failed: {}", plan.failed_count);
    println!(
        "  validation: passed ({} study entries)",
        validation.study_entries.len()
    );
    println!("  evidence: {}", plan.evidence_artifact_path);
    for entry in &plan.plan_entries {
        println!(
            "  {}: {} ({})",
            entry.study_id, entry.run_operation, entry.run_op_version
        );
    }
}

fn print_sweep_validation_failure(validation: &AnalysisStudySweepValidateData) {
    println!("FAILED {}", validation.sweep_id);
    println!("  evidence: {}", validation.evidence_artifact_path);
    for entry in &validation.study_entries {
        if entry.valid {
            continue;
        }
        println!("  {}", entry.study_id);
        for issue in &entry.issues {
            println!("    {}: {}", issue.code, issue.message);
        }
    }
}

fn print_payload<T: Serialize>(
    document_kind: &'static str,
    status: &'static str,
    data: &T,
    include_metadata: bool,
) -> Result<()> {
    if include_metadata {
        println!(
            "{}",
            serde_json::to_string_pretty(&json!({
                "document_kind": document_kind,
                "status": status,
                "data": data,
            }))
            .context("Failed to serialize check result")?
        );
    } else {
        println!(
            "{}",
            serde_json::to_string_pretty(data).context("Failed to serialize check result")?
        );
    }
    Ok(())
}

fn is_matlab_file(path: &Path) -> bool {
    path.extension()
        .and_then(|ext| ext.to_str())
        .is_some_and(|ext| ext.eq_ignore_ascii_case("m"))
}

fn report_operation_error(error: OperationErrorEnvelope) -> anyhow::Error {
    match serde_json::to_string_pretty(&error) {
        Ok(payload) => eprintln!("{payload}"),
        Err(_) => eprintln!("{}: {}", error.error_code, error.message.replace('\n', " ")),
    }
    AlreadyReportedCliError.into()
}