use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, OnceLock, RwLock};
#[cfg(test)]
use std::sync::{Mutex, MutexGuard};

use self::capture::DEFAULT_SVG_CAPTURE_ADAPTER;
use chrono::Utc;
use runmat_geometry_core::{
    AssemblyNode, DiagnosticSeverity, EntityIdRange, EntityKind, EntityRef, GeometryAsset,
    GeometrySource, MeshKind, Region, SourceGeometry, SourceGeometryKind, TessellationProfile,
    UnitSystem,
};
use runmat_geometry_io::{
    import::GeometryImportError, import_geometry_with_context, GeometryFormat,
    GeometryImportContext, GeometryImportOptions,
};
use runmat_geometry_ops::{compute_stats, find_region, GeometryStats, QueryError};
use runmat_meshing_core::{
    prepare_geometry_for_analysis, MeshingOptions, MeshingPrepResult, MeshingProfile,
};
use serde::{Deserialize, Serialize};

use crate::operations::{
    operation_error, OperationContext, OperationEnvelope, OperationErrorEnvelope,
    OperationErrorSeverity, OperationErrorSpec, OperationErrorType,
};
use crate::{build_runtime_error, BuiltinResult};

const GEOMETRY_INSPECT_OPERATION: &str = "geometry.inspect";
const GEOMETRY_INSPECT_OP_VERSION: &str = "geometry.inspect/v1";
const GEOMETRY_LOAD_OPERATION: &str = "geometry.load";
const GEOMETRY_LOAD_OP_VERSION: &str = "geometry.load/v1";
const GEOMETRY_COMPUTE_STATS_OPERATION: &str = "geometry.compute_stats";
const GEOMETRY_COMPUTE_STATS_OP_VERSION: &str = "geometry.compute_stats/v1";
const GEOMETRY_LIST_REGIONS_OPERATION: &str = "geometry.list_regions";
const GEOMETRY_LIST_REGIONS_OP_VERSION: &str = "geometry.list_regions/v1";
const GEOMETRY_QUERY_ENTITIES_OPERATION: &str = "geometry.query_entities";
const GEOMETRY_QUERY_ENTITIES_OP_VERSION: &str = "geometry.query_entities/v1";
const GEOMETRY_CAPTURE_VIEW_OPERATION: &str = "geometry.capture_view";
const GEOMETRY_CAPTURE_VIEW_OP_VERSION: &str = "geometry.capture_view/v1";
const GEOMETRY_PREP_FOR_ANALYSIS_OPERATION: &str = "geometry.prep_for_analysis";
const GEOMETRY_PREP_FOR_ANALYSIS_OP_VERSION: &str = "geometry.prep_for_analysis/v1";
const GEOMETRY_PREP_ARTIFACT_HEALTH_OPERATION: &str = "geometry.prep_artifact_health";
const GEOMETRY_PREP_ARTIFACT_HEALTH_OP_VERSION: &str = "geometry.prep_artifact_health/v1";
const DEFAULT_QUERY_LIMIT: usize = 2048;
const DEFAULT_MAPPING_RANGE_PREVIEW_LIMIT: usize = 8;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeometryInspectResult {
    pub format: String,
    pub byte_count: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeometryRegionsResult {
    pub regions: Vec<Region>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeometryEntityQuery {
    pub region_id: Option<String>,
    pub mesh_id: Option<String>,
    pub entity_kind: EntityKind,
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeometryEntityQueryResult {
    pub entities: Vec<EntityRef>,
    pub truncated: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GeometryBoundsSummary {
    pub min: [f64; 3],
    pub max: [f64; 3],
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GeometryMeshSummary {
    pub mesh_id: String,
    pub kind: MeshKind,
    pub vertex_count: u64,
    pub element_count: u64,
    pub surface_vertex_count: Option<u64>,
    pub surface_triangle_count: Option<u64>,
    pub bounds: Option<GeometryBoundsSummary>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GeometryRegionMappingSummaryEntry {
    pub region_id: String,
    pub mesh_id: String,
    pub entity_kind: EntityKind,
    pub range_count: usize,
    pub entity_count: u64,
    pub range_preview: Vec<EntityIdRange>,
    pub truncated: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GeometryRegionMappingSummary {
    pub mapping_count: usize,
    pub mapped_region_count: usize,
    pub total_entity_count: u64,
    pub range_preview_limit: usize,
    pub entries: Vec<GeometryRegionMappingSummaryEntry>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GeometryCadRegionStatus {
    NotCad,
    MetadataOnly,
    GenericFaceTopology,
    SemanticRegions,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GeometryCadSummary {
    pub backend: Option<String>,
    pub source_format: Option<String>,
    pub face_region_count: usize,
    pub mapped_face_region_count: usize,
    pub semantic_region_count: usize,
    pub mapped_semantic_region_count: usize,
    pub region_status: GeometryCadRegionStatus,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GeometryAssetSummary {
    pub geometry_id: String,
    pub revision: u32,
    pub source: GeometrySource,
    pub source_geometry: SourceGeometry,
    pub tessellation_profile: TessellationProfile,
    pub units: UnitSystem,
    pub meshes: Vec<GeometryMeshSummary>,
    pub mapping_summary: GeometryRegionMappingSummary,
    pub cad: GeometryCadSummary,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeometryCaptureViewSpec {
    pub format: String,
    pub width: u32,
    pub height: u32,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeometryCaptureViewResult {
    pub format: String,
    pub width: u32,
    pub height: u32,
    pub payload: Vec<u8>,
}

#[cfg(feature = "plot-core")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GeometryPreviewPresentation {
    Analysis,
    Cad,
}

#[cfg(feature = "plot-core")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeometryPreviewFigureOptions {
    pub edge_overlay_triangle_limit: usize,
    pub presentation: GeometryPreviewPresentation,
    pub xray: bool,
    pub allow_create_fea_study: bool,
}

#[cfg(feature = "plot-core")]
impl Default for GeometryPreviewFigureOptions {
    fn default() -> Self {
        Self {
            edge_overlay_triangle_limit: 250_000,
            presentation: GeometryPreviewPresentation::Analysis,
            xray: false,
            allow_create_fea_study: false,
        }
    }
}

#[cfg(feature = "plot-core")]
impl GeometryPreviewFigureOptions {
    pub fn cad_preview() -> Self {
        Self {
            edge_overlay_triangle_limit: 250_000,
            presentation: GeometryPreviewPresentation::Cad,
            xray: false,
            allow_create_fea_study: false,
        }
    }
}

#[cfg(feature = "plot-core")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeometryPreviewSceneOptions {
    pub triangles_per_chunk: usize,
    pub presentation: GeometryPreviewPresentation,
    pub xray: bool,
    pub allow_create_fea_study: bool,
}

#[cfg(feature = "plot-core")]
impl Default for GeometryPreviewSceneOptions {
    fn default() -> Self {
        Self {
            triangles_per_chunk: 128_000,
            presentation: GeometryPreviewPresentation::Cad,
            xray: false,
            allow_create_fea_study: false,
        }
    }
}

#[cfg(feature = "plot-core")]
const CAD_DEFAULT_FACE_COLOR: glam::Vec4 = glam::Vec4::new(0.66, 0.72, 0.80, 1.0);
#[cfg(feature = "plot-core")]
const CAD_FEATURE_EDGE_COLOR: glam::Vec4 = glam::Vec4::new(0.08, 0.10, 0.13, 1.0);

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GeometryPrepProfile {
    SurfaceOnly,
    AnalysisReady,
    AdaptiveRefine,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeometryPrepForAnalysisSpec {
    pub profile: GeometryPrepProfile,
    pub target_element_budget: usize,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GeometryPrepForAnalysisResult {
    pub prep_artifact_id: String,
    pub prep: MeshingPrepResult,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StoredGeometryPrepArtifact {
    pub prep_artifact_id: String,
    pub schema_version: String,
    pub created_at: String,
    pub source_geometry_id: String,
    pub source_geometry_revision: u32,
    pub prep: MeshingPrepResult,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PrepArtifactMetrics {
    pub created_count: u64,
    pub loaded_count: u64,
    pub pruned_count: u64,
    pub stale_reject_count: u64,
    pub mismatch_reject_count: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeometryPrepArtifactHealthQuery {
    pub include_per_geometry: bool,
}

impl Default for GeometryPrepArtifactHealthQuery {
    fn default() -> Self {
        Self {
            include_per_geometry: true,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeometryPrepArtifactHealthEntry {
    pub geometry_id: String,
    pub latest_revision: u32,
    pub artifact_count: usize,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GeometryPrepArtifactHealthResult {
    pub schema_version: String,
    pub current_artifact_count: usize,
    pub age_p50_seconds: Option<f64>,
    pub age_p95_seconds: Option<f64>,
    pub metrics: PrepArtifactMetrics,
    pub per_geometry: Vec<GeometryPrepArtifactHealthEntry>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct GeometryPrepArtifactConfig {
    pub artifact_root: Option<PathBuf>,
    pub max_artifacts: Option<usize>,
    pub max_artifacts_per_geometry: Option<usize>,
    pub max_age_seconds: Option<u64>,
    pub require_latest_revision: Option<bool>,
}

type PrepStore = Arc<RwLock<HashMap<String, StoredGeometryPrepArtifact>>>;

fn prep_store() -> &'static PrepStore {
    static STORE: OnceLock<PrepStore> = OnceLock::new();
    STORE.get_or_init(|| Arc::new(RwLock::new(HashMap::new())))
}

fn prep_artifact_counter() -> &'static AtomicU64 {
    static COUNTER: OnceLock<AtomicU64> = OnceLock::new();
    COUNTER.get_or_init(|| AtomicU64::new(1))
}

fn prep_metrics() -> &'static Arc<RwLock<PrepArtifactMetrics>> {
    static METRICS: OnceLock<Arc<RwLock<PrepArtifactMetrics>>> = OnceLock::new();
    METRICS.get_or_init(|| Arc::new(RwLock::new(PrepArtifactMetrics::default())))
}

fn prep_config() -> &'static RwLock<GeometryPrepArtifactConfig> {
    static CONFIG: OnceLock<RwLock<GeometryPrepArtifactConfig>> = OnceLock::new();
    CONFIG.get_or_init(|| RwLock::new(GeometryPrepArtifactConfig::default()))
}

fn current_prep_config() -> GeometryPrepArtifactConfig {
    prep_config()
        .read()
        .map(|guard| guard.clone())
        .unwrap_or_default()
}

pub fn configure_prep_artifacts(config: GeometryPrepArtifactConfig) -> Result<(), String> {
    let mut guard = prep_config()
        .write()
        .map_err(|_| "geometry prep artifact config lock poisoned".to_string())?;
    *guard = config;
    Ok(())
}

fn increment_metric(f: impl FnOnce(&mut PrepArtifactMetrics)) {
    if let Ok(mut metrics) = prep_metrics().write() {
        f(&mut metrics);
    }
}

fn prep_artifact_root() -> Option<PathBuf> {
    current_prep_config().artifact_root.or_else(|| {
        std::env::var("RUNMAT_GEOMETRY_PREP_ARTIFACT_ROOT")
            .ok()
            .map(PathBuf::from)
    })
}

pub(crate) fn require_latest_prep_revision() -> bool {
    current_prep_config()
        .require_latest_revision
        .unwrap_or_else(|| {
            std::env::var("RUNMAT_GEOMETRY_PREP_REQUIRE_LATEST_REVISION")
                .ok()
                .map(|value| {
                    matches!(
                        value.to_ascii_lowercase().as_str(),
                        "1" | "true" | "yes" | "on"
                    )
                })
                .unwrap_or(true)
        })
}

fn prep_artifact_path(root: &Path, prep_artifact_id: &str) -> PathBuf {
    root.join("prep").join(format!("{prep_artifact_id}.json"))
}

fn prep_artifact_id_fragment(value: &str) -> String {
    let mut fragment = String::with_capacity(value.len());
    for byte in value.bytes() {
        match byte {
            b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'.' | b'_' | b'-' => {
                fragment.push(byte as char);
            }
            _ => {
                if !fragment.ends_with('_') {
                    fragment.push('_');
                }
            }
        }
    }
    let fragment = fragment.trim_matches('_').to_string();
    if fragment.is_empty() {
        "geometry".to_string()
    } else {
        fragment
    }
}

fn fs_create_dir_all(path: impl Into<PathBuf>) -> std::io::Result<()> {
    runmat_filesystem::create_dir_all(path.into())
}

fn fs_read(path: impl Into<PathBuf>) -> std::io::Result<Vec<u8>> {
    runmat_filesystem::read(path.into())
}

fn fs_write(path: impl Into<PathBuf>, bytes: &[u8]) -> std::io::Result<()> {
    runmat_filesystem::write(path.into(), bytes)
}

fn fs_remove_file(path: impl Into<PathBuf>) -> std::io::Result<()> {
    match runmat_filesystem::remove_file(path.into()) {
        Ok(()) => Ok(()),
        Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
        Err(err) => Err(err),
    }
}

fn fs_read_dir(path: impl Into<PathBuf>) -> std::io::Result<Vec<runmat_filesystem::DirEntry>> {
    runmat_filesystem::read_dir(path.into())
}

fn fs_exists(path: impl Into<PathBuf>) -> std::io::Result<bool> {
    match runmat_filesystem::metadata(path.into()) {
        Ok(_) => Ok(true),
        Err(err) if err.kind() == ErrorKind::NotFound => Ok(false),
        Err(err) => Err(err),
    }
}

#[derive(Debug, Clone, Copy)]
struct PrepArtifactRetentionPolicy {
    max_artifacts: usize,
    max_artifacts_per_geometry: usize,
    max_age_seconds: u64,
}

impl PrepArtifactRetentionPolicy {
    fn current() -> Self {
        let config = current_prep_config();
        Self {
            max_artifacts: config.max_artifacts.unwrap_or_else(|| {
                std::env::var("RUNMAT_GEOMETRY_PREP_MAX_ARTIFACTS")
                    .ok()
                    .and_then(|value| value.parse::<usize>().ok())
                    .unwrap_or(0)
            }),
            max_artifacts_per_geometry: config.max_artifacts_per_geometry.unwrap_or_else(|| {
                std::env::var("RUNMAT_GEOMETRY_PREP_MAX_ARTIFACTS_PER_GEOMETRY")
                    .ok()
                    .and_then(|value| value.parse::<usize>().ok())
                    .unwrap_or(0)
            }),
            max_age_seconds: config.max_age_seconds.unwrap_or_else(|| {
                std::env::var("RUNMAT_GEOMETRY_PREP_MAX_AGE_SECONDS")
                    .ok()
                    .and_then(|value| value.parse::<u64>().ok())
                    .unwrap_or(0)
            }),
        }
    }
}

fn persist_prep_artifact(
    geometry: &GeometryAsset,
    prep: MeshingPrepResult,
) -> Result<StoredGeometryPrepArtifact, String> {
    let prep_artifact_id = format!(
        "prep_{}_{}_{}",
        prep_artifact_id_fragment(&geometry.geometry_id),
        geometry.revision,
        prep_artifact_counter().fetch_add(1, Ordering::Relaxed)
    );
    let artifact = StoredGeometryPrepArtifact {
        prep_artifact_id: prep_artifact_id.clone(),
        schema_version: "geometry_prep_artifact/v1".to_string(),
        created_at: Utc::now().to_rfc3339(),
        source_geometry_id: geometry.geometry_id.clone(),
        source_geometry_revision: geometry.revision,
        prep,
    };

    prep_store()
        .write()
        .map_err(|_| "geometry prep artifact store lock poisoned".to_string())?
        .insert(prep_artifact_id.clone(), artifact.clone());
    increment_metric(|metrics| metrics.created_count = metrics.created_count.saturating_add(1));
    tracing::info!(
        target: "runmat_geometry",
        "prep_artifact_created id={} geometry_id={} revision={}",
        prep_artifact_id,
        geometry.geometry_id,
        geometry.revision
    );

    if let Some(root) = prep_artifact_root() {
        let path = prep_artifact_path(&root, &prep_artifact_id);
        if let Some(parent) = path.parent() {
            fs_create_dir_all(parent)
                .map_err(|err| format!("failed to create prep artifact directory: {err}"))?;
        }
        let bytes = serde_json::to_vec_pretty(&artifact)
            .map_err(|err| format!("failed to encode prep artifact: {err}"))?;
        fs_write(&path, &bytes).map_err(|err| format!("failed to write prep artifact: {err}"))?;
    }

    prune_prep_artifacts(PrepArtifactRetentionPolicy::current())?;

    Ok(artifact)
}

pub(crate) fn load_prep_artifact(
    prep_artifact_id: &str,
) -> Result<Option<StoredGeometryPrepArtifact>, String> {
    if let Some(artifact) = prep_store()
        .read()
        .map_err(|_| "geometry prep artifact store lock poisoned".to_string())?
        .get(prep_artifact_id)
        .cloned()
    {
        return Ok(Some(artifact));
    }

    let Some(root) = prep_artifact_root() else {
        return Ok(None);
    };
    let path = prep_artifact_path(&root, prep_artifact_id);
    if !fs_exists(&path).map_err(|err| format!("failed to inspect prep artifact: {err}"))? {
        return Ok(None);
    }
    let bytes = fs_read(&path).map_err(|err| format!("failed to read prep artifact: {err}"))?;
    let artifact = serde_json::from_slice::<StoredGeometryPrepArtifact>(&bytes)
        .map_err(|err| format!("failed to decode prep artifact: {err}"))?;
    prep_store()
        .write()
        .map_err(|_| "geometry prep artifact store lock poisoned".to_string())?
        .insert(prep_artifact_id.to_string(), artifact.clone());
    increment_metric(|metrics| metrics.loaded_count = metrics.loaded_count.saturating_add(1));
    tracing::info!(
        target: "runmat_geometry",
        "prep_artifact_loaded id={} geometry_id={} revision={}",
        prep_artifact_id,
        artifact.source_geometry_id,
        artifact.source_geometry_revision
    );
    prune_prep_artifacts(PrepArtifactRetentionPolicy::current())?;
    Ok(Some(artifact))
}

pub(crate) fn record_prep_stale_reject() {
    increment_metric(|metrics| {
        metrics.stale_reject_count = metrics.stale_reject_count.saturating_add(1)
    });
    tracing::warn!(target: "runmat_geometry", "prep_artifact_rejected reason=stale");
}

pub(crate) fn record_prep_mismatch_reject() {
    increment_metric(|metrics| {
        metrics.mismatch_reject_count = metrics.mismatch_reject_count.saturating_add(1)
    });
    tracing::warn!(
        target: "runmat_geometry",
        "prep_artifact_rejected reason=mismatch"
    );
}

pub fn geometry_prep_artifact_health_op(
    query: GeometryPrepArtifactHealthQuery,
    context: OperationContext,
) -> Result<OperationEnvelope<GeometryPrepArtifactHealthResult>, OperationErrorEnvelope> {
    let artifacts = list_prep_artifacts().map_err(|err| {
        operation_error(
            GEOMETRY_PREP_ARTIFACT_HEALTH_OPERATION,
            GEOMETRY_PREP_ARTIFACT_HEALTH_OP_VERSION,
            &context,
            OperationErrorSpec {
                error_code: "RM.GEOMETRY.PREP_ARTIFACT_HEALTH.STORE_FAILED",
                error_type: OperationErrorType::Internal,
                retryable: true,
                severity: OperationErrorSeverity::Error,
            },
            format!("failed to list prep artifacts: {err}"),
            BTreeMap::new(),
        )
    })?;

    let now = Utc::now();
    let mut age_seconds = Vec::new();
    let mut per_geometry_map: HashMap<String, (u32, usize)> = HashMap::new();
    for artifact in &artifacts {
        if let Ok(created) = chrono::DateTime::parse_from_rfc3339(&artifact.created_at) {
            let age = now.signed_duration_since(created.with_timezone(&Utc));
            age_seconds.push(age.num_seconds().max(0) as f64);
        }
        let entry = per_geometry_map
            .entry(artifact.source_geometry_id.clone())
            .or_insert((artifact.source_geometry_revision, 0));
        if artifact.source_geometry_revision > entry.0 {
            entry.0 = artifact.source_geometry_revision;
        }
        entry.1 = entry.1.saturating_add(1);
    }
    age_seconds.sort_by(|a, b| a.total_cmp(b));

    let per_geometry = if query.include_per_geometry {
        let mut values = per_geometry_map
            .into_iter()
            .map(|(geometry_id, (latest_revision, artifact_count))| {
                GeometryPrepArtifactHealthEntry {
                    geometry_id,
                    latest_revision,
                    artifact_count,
                }
            })
            .collect::<Vec<_>>();
        values.sort_by(|a, b| a.geometry_id.cmp(&b.geometry_id));
        values
    } else {
        Vec::new()
    };

    let metrics = prep_metrics()
        .read()
        .map_err(|_| {
            operation_error(
                GEOMETRY_PREP_ARTIFACT_HEALTH_OPERATION,
                GEOMETRY_PREP_ARTIFACT_HEALTH_OP_VERSION,
                &context,
                OperationErrorSpec {
                    error_code: "RM.GEOMETRY.PREP_ARTIFACT_HEALTH.STORE_FAILED",
                    error_type: OperationErrorType::Internal,
                    retryable: true,
                    severity: OperationErrorSeverity::Error,
                },
                "geometry prep metrics store lock poisoned",
                BTreeMap::new(),
            )
        })?
        .clone();

    Ok(OperationEnvelope::new(
        GEOMETRY_PREP_ARTIFACT_HEALTH_OPERATION,
        GEOMETRY_PREP_ARTIFACT_HEALTH_OP_VERSION,
        &context,
        GeometryPrepArtifactHealthResult {
            schema_version: "geometry-prep-artifact-health/v1".to_string(),
            current_artifact_count: artifacts.len(),
            age_p50_seconds: percentile(&age_seconds, 0.5),
            age_p95_seconds: percentile(&age_seconds, 0.95),
            metrics,
            per_geometry,
        },
    ))
}

fn percentile(sorted: &[f64], ratio: f64) -> Option<f64> {
    if sorted.is_empty() {
        return None;
    }
    let index = ((sorted.len() - 1) as f64 * ratio.clamp(0.0, 1.0)).round() as usize;
    sorted.get(index).copied()
}

pub(crate) fn latest_prep_revision_for_geometry(geometry_id: &str) -> Result<Option<u32>, String> {
    let mut revisions = list_prep_artifacts()?
        .into_iter()
        .filter(|artifact| artifact.source_geometry_id == geometry_id)
        .map(|artifact| artifact.source_geometry_revision)
        .collect::<Vec<_>>();
    revisions.sort_unstable();
    Ok(revisions.pop())
}

fn list_prep_artifacts() -> Result<Vec<StoredGeometryPrepArtifact>, String> {
    let mut artifacts = prep_store()
        .read()
        .map_err(|_| "geometry prep artifact store lock poisoned".to_string())?
        .values()
        .cloned()
        .collect::<Vec<_>>();
    if artifacts.is_empty() {
        if let Some(root) = prep_artifact_root() {
            let prep_dir = root.join("prep");
            if fs_exists(&prep_dir)
                .map_err(|err| format!("failed to inspect prep artifacts: {err}"))?
            {
                for entry in fs_read_dir(&prep_dir)
                    .map_err(|err| format!("failed to scan prep artifacts: {err}"))?
                {
                    let path = entry.path().to_path_buf();
                    if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
                        continue;
                    }
                    let bytes = fs_read(&path)
                        .map_err(|err| format!("failed to read prep artifact: {err}"))?;
                    if let Ok(artifact) =
                        serde_json::from_slice::<StoredGeometryPrepArtifact>(&bytes)
                    {
                        artifacts.push(artifact);
                    }
                }
            }
        }
    }
    Ok(artifacts)
}

fn prune_prep_artifacts(policy: PrepArtifactRetentionPolicy) -> Result<(), String> {
    if policy.max_artifacts == 0
        && policy.max_artifacts_per_geometry == 0
        && policy.max_age_seconds == 0
    {
        return Ok(());
    }

    let now = Utc::now();
    let mut artifacts = list_prep_artifacts()?;
    artifacts.sort_by(|a, b| b.created_at.cmp(&a.created_at));

    let mut remove_ids = Vec::new();
    if policy.max_age_seconds > 0 {
        for artifact in &artifacts {
            if let Ok(created) = chrono::DateTime::parse_from_rfc3339(&artifact.created_at) {
                let age = now.signed_duration_since(created.with_timezone(&Utc));
                if age.num_seconds().max(0) as u64 > policy.max_age_seconds {
                    remove_ids.push(artifact.prep_artifact_id.clone());
                }
            }
        }
    }

    if policy.max_artifacts_per_geometry > 0 {
        let mut per_geometry_counts: HashMap<String, usize> = HashMap::new();
        for artifact in &artifacts {
            let count = per_geometry_counts
                .entry(artifact.source_geometry_id.clone())
                .or_default();
            *count += 1;
            if *count > policy.max_artifacts_per_geometry {
                remove_ids.push(artifact.prep_artifact_id.clone());
            }
        }
    }

    if policy.max_artifacts > 0 {
        for (index, artifact) in artifacts.iter().enumerate() {
            if index >= policy.max_artifacts {
                remove_ids.push(artifact.prep_artifact_id.clone());
            }
        }
    }

    remove_ids.sort();
    remove_ids.dedup();
    if remove_ids.is_empty() {
        return Ok(());
    }

    {
        let mut store = prep_store()
            .write()
            .map_err(|_| "geometry prep artifact store lock poisoned".to_string())?;
        for id in &remove_ids {
            store.remove(id);
        }
    }

    if let Some(root) = prep_artifact_root() {
        for id in &remove_ids {
            let path = prep_artifact_path(&root, id);
            let _ = fs_remove_file(path);
        }
    }

    increment_metric(|metrics| {
        metrics.pruned_count = metrics.pruned_count.saturating_add(remove_ids.len() as u64)
    });
    tracing::info!(
        target: "runmat_geometry",
        "prep_artifact_pruned count={}",
        remove_ids.len()
    );

    Ok(())
}

#[doc(hidden)]
pub fn reset_prep_artifact_store_for_tests() {
    if let Ok(mut store) = prep_store().write() {
        store.clear();
    }
    prep_artifact_counter().store(1, Ordering::Relaxed);
    if let Ok(mut metrics) = prep_metrics().write() {
        *metrics = PrepArtifactMetrics::default();
    }
    if let Ok(mut config) = prep_config().write() {
        *config = GeometryPrepArtifactConfig::default();
    }
}

#[cfg(test)]
pub(crate) fn prep_artifact_test_guard() -> MutexGuard<'static, ()> {
    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    LOCK.get_or_init(|| Mutex::new(()))
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
}

impl Default for GeometryPrepForAnalysisSpec {
    fn default() -> Self {
        Self {
            profile: GeometryPrepProfile::AnalysisReady,
            target_element_budget: 250_000,
        }
    }
}

pub trait GeometryViewCaptureAdapter {
    fn adapter_name(&self) -> &'static str;
    fn capture(
        &self,
        asset: &GeometryAsset,
        view_spec: &GeometryCaptureViewSpec,
    ) -> Result<GeometryCaptureViewResult, String>;
}

thread_local! {
    static GEOMETRY_CAPTURE_ADAPTER: RefCell<Option<&'static dyn GeometryViewCaptureAdapter>> =
        RefCell::new(None);
}

mod capture;

pub struct ThreadGeometryCaptureAdapterGuard {
    previous: Option<&'static dyn GeometryViewCaptureAdapter>,
}

impl ThreadGeometryCaptureAdapterGuard {
    pub fn set(adapter: Option<&'static dyn GeometryViewCaptureAdapter>) -> Self {
        let previous = GEOMETRY_CAPTURE_ADAPTER.with(|slot| slot.replace(adapter));
        Self { previous }
    }
}

impl Drop for ThreadGeometryCaptureAdapterGuard {
    fn drop(&mut self) {
        GEOMETRY_CAPTURE_ADAPTER.with(|slot| {
            slot.replace(self.previous.take());
        });
    }
}

pub fn geometry_inspect_op(
    path: &str,
    bytes: &[u8],
    context: OperationContext,
) -> Result<OperationEnvelope<GeometryInspectResult>, OperationErrorEnvelope> {
    let format = runmat_geometry_io::detect_geometry_format(path, bytes);
    let data = GeometryInspectResult {
        format: format_name(format).to_string(),
        byte_count: bytes.len(),
    };
    Ok(OperationEnvelope::new(
        GEOMETRY_INSPECT_OPERATION,
        GEOMETRY_INSPECT_OP_VERSION,
        &context,
        data,
    ))
}

pub fn geometry_inspect(path: &str, bytes: &[u8]) -> BuiltinResult<GeometryInspectResult> {
    let envelope =
        geometry_inspect_op(path, bytes, OperationContext::new(None, None)).map_err(|error| {
            build_runtime_error(error.message)
                .with_builtin(GEOMETRY_INSPECT_OPERATION)
                .with_identifier("RunMat:GeometryInspectFailed")
                .build()
        })?;
    Ok(envelope.data)
}

pub fn geometry_load_op(
    path: &str,
    bytes: &[u8],
    context: OperationContext,
) -> Result<OperationEnvelope<GeometryAsset>, OperationErrorEnvelope> {
    geometry_load_with_options_op(path, bytes, GeometryImportOptions::default(), context)
}

pub fn geometry_load_with_options_op(
    path: &str,
    bytes: &[u8],
    options: GeometryImportOptions,
    context: OperationContext,
) -> Result<OperationEnvelope<GeometryAsset>, OperationErrorEnvelope> {
    let import_context = current_geometry_import_context();
    let imported = import_geometry_with_context(path, bytes, options, &import_context)
        .map_err(|error| map_geometry_load_error(path, error, &context))?;
    Ok(OperationEnvelope::new(
        GEOMETRY_LOAD_OPERATION,
        GEOMETRY_LOAD_OP_VERSION,
        &context,
        imported.asset,
    ))
}

pub fn geometry_load(path: &str, bytes: &[u8]) -> BuiltinResult<GeometryAsset> {
    let envelope =
        geometry_load_op(path, bytes, OperationContext::new(None, None)).map_err(|error| {
            build_runtime_error(error.message)
                .with_builtin(GEOMETRY_LOAD_OPERATION)
                .with_identifier("RunMat:GeometryLoadFailed")
                .build()
        })?;
    Ok(envelope.data)
}

fn current_geometry_import_context() -> GeometryImportContext {
    crate::interrupt::current_interrupt()
        .map(GeometryImportContext::with_cancellation)
        .unwrap_or_default()
}

pub fn geometry_compute_stats_op(
    asset: &GeometryAsset,
    context: OperationContext,
) -> Result<OperationEnvelope<GeometryStats>, OperationErrorEnvelope> {
    Ok(OperationEnvelope::new(
        GEOMETRY_COMPUTE_STATS_OPERATION,
        GEOMETRY_COMPUTE_STATS_OP_VERSION,
        &context,
        compute_stats(asset),
    ))
}

pub fn geometry_compute_stats(asset: &GeometryAsset) -> BuiltinResult<GeometryStats> {
    let envelope =
        geometry_compute_stats_op(asset, OperationContext::new(None, None)).map_err(|error| {
            build_runtime_error(error.message)
                .with_builtin(GEOMETRY_COMPUTE_STATS_OPERATION)
                .with_identifier("RunMat:GeometryStatsFailed")
                .build()
        })?;
    Ok(envelope.data)
}

pub fn geometry_asset_summary(asset: &GeometryAsset) -> GeometryAssetSummary {
    geometry_asset_summary_with_options(asset, DEFAULT_MAPPING_RANGE_PREVIEW_LIMIT)
}

pub fn geometry_asset_summary_with_options(
    asset: &GeometryAsset,
    range_preview_limit: usize,
) -> GeometryAssetSummary {
    GeometryAssetSummary {
        geometry_id: asset.geometry_id.clone(),
        revision: asset.revision,
        source: asset.source.clone(),
        source_geometry: asset.source_geometry.clone(),
        tessellation_profile: asset.tessellation_profile.clone(),
        units: asset.units,
        meshes: mesh_summaries(asset),
        mapping_summary: region_mapping_summary(asset, range_preview_limit),
        cad: cad_summary(asset),
    }
}

fn mesh_summaries(asset: &GeometryAsset) -> Vec<GeometryMeshSummary> {
    asset
        .meshes
        .iter()
        .map(|mesh| {
            let surface_mesh = asset
                .surface_meshes
                .iter()
                .find(|surface| surface.mesh_id == mesh.mesh_id);
            GeometryMeshSummary {
                mesh_id: mesh.mesh_id.clone(),
                kind: mesh.kind,
                vertex_count: mesh.vertex_count,
                element_count: mesh.element_count,
                surface_vertex_count: surface_mesh.map(|surface| surface.vertices.len() as u64),
                surface_triangle_count: surface_mesh.map(|surface| surface.triangles.len() as u64),
                bounds: surface_mesh.and_then(|surface| bounds_for_vertices(&surface.vertices)),
            }
        })
        .collect()
}

fn bounds_for_vertices(vertices: &[[f64; 3]]) -> Option<GeometryBoundsSummary> {
    let first = vertices.first().copied()?;
    let mut min = first;
    let mut max = first;
    for vertex in vertices.iter().skip(1) {
        for axis in 0..3 {
            min[axis] = min[axis].min(vertex[axis]);
            max[axis] = max[axis].max(vertex[axis]);
        }
    }
    Some(GeometryBoundsSummary { min, max })
}

fn region_mapping_summary(
    asset: &GeometryAsset,
    range_preview_limit: usize,
) -> GeometryRegionMappingSummary {
    let mut mapped_regions = BTreeSet::new();
    let mut total_entity_count = 0_u64;
    let entries = asset
        .region_entity_mappings
        .iter()
        .map(|mapping| {
            mapped_regions.insert(mapping.region_id.clone());
            let entity_count = mapping.entity_count();
            total_entity_count = total_entity_count.saturating_add(entity_count);
            let range_count = mapping.ranges.len();
            GeometryRegionMappingSummaryEntry {
                region_id: mapping.region_id.clone(),
                mesh_id: mapping.mesh_id.clone(),
                entity_kind: mapping.entity_kind,
                range_count,
                entity_count,
                range_preview: mapping
                    .ranges
                    .iter()
                    .take(range_preview_limit)
                    .copied()
                    .collect(),
                truncated: range_count > range_preview_limit,
            }
        })
        .collect();

    GeometryRegionMappingSummary {
        mapping_count: asset.region_entity_mappings.len(),
        mapped_region_count: mapped_regions.len(),
        total_entity_count,
        range_preview_limit,
        entries,
    }
}

fn cad_summary(asset: &GeometryAsset) -> GeometryCadSummary {
    let importer_parts = asset.source.importer_version.split('/').collect::<Vec<_>>();
    let backend = match importer_parts.as_slice() {
        ["cad", backend, ..] => Some((*backend).to_string()),
        ["step", ..] if asset.source_geometry.kind == SourceGeometryKind::Cad => {
            Some("metadata".to_string())
        }
        _ => None,
    };
    let source_format = match importer_parts.as_slice() {
        ["cad", _, format, ..] => Some((*format).to_string()),
        [format, ..] if asset.source_geometry.kind == SourceGeometryKind::Cad => {
            Some((*format).to_string())
        }
        _ => None,
    };
    let face_region_ids = asset
        .regions
        .iter()
        .filter(|region| region.tag.as_deref() == Some("occt_face"))
        .map(|region| region.region_id.as_str())
        .collect::<BTreeSet<_>>();
    let mapped_face_region_ids = asset
        .region_entity_mappings
        .iter()
        .filter_map(|mapping| {
            (mapping.entity_kind == EntityKind::Face
                && face_region_ids.contains(mapping.region_id.as_str()))
            .then_some(mapping.region_id.as_str())
        })
        .collect::<BTreeSet<_>>();
    let semantic_region_ids = asset
        .regions
        .iter()
        .filter(|region| {
            region.cad_ownership.is_some()
                && region
                    .tag
                    .as_deref()
                    .is_some_and(|tag| tag.starts_with("cad_"))
        })
        .map(|region| region.region_id.as_str())
        .collect::<BTreeSet<_>>();
    let mapped_semantic_region_ids = asset
        .region_entity_mappings
        .iter()
        .filter_map(|mapping| {
            (mapping.entity_kind == EntityKind::Face
                && semantic_region_ids.contains(mapping.region_id.as_str()))
            .then_some(mapping.region_id.as_str())
        })
        .collect::<BTreeSet<_>>();
    let region_status = if asset.source_geometry.kind != SourceGeometryKind::Cad {
        GeometryCadRegionStatus::NotCad
    } else if mapped_face_region_ids.is_empty() {
        GeometryCadRegionStatus::MetadataOnly
    } else if !mapped_semantic_region_ids.is_empty() {
        GeometryCadRegionStatus::SemanticRegions
    } else {
        GeometryCadRegionStatus::GenericFaceTopology
    };

    GeometryCadSummary {
        backend,
        source_format,
        face_region_count: face_region_ids.len(),
        mapped_face_region_count: mapped_face_region_ids.len(),
        semantic_region_count: semantic_region_ids.len(),
        mapped_semantic_region_count: mapped_semantic_region_ids.len(),
        region_status,
    }
}

pub fn geometry_list_regions_op(
    asset: &GeometryAsset,
    context: OperationContext,
) -> Result<OperationEnvelope<GeometryRegionsResult>, OperationErrorEnvelope> {
    Ok(OperationEnvelope::new(
        GEOMETRY_LIST_REGIONS_OPERATION,
        GEOMETRY_LIST_REGIONS_OP_VERSION,
        &context,
        GeometryRegionsResult {
            regions: asset.regions.clone(),
        },
    ))
}

pub fn geometry_list_regions(asset: &GeometryAsset) -> BuiltinResult<GeometryRegionsResult> {
    let envelope =
        geometry_list_regions_op(asset, OperationContext::new(None, None)).map_err(|error| {
            build_runtime_error(error.message)
                .with_builtin(GEOMETRY_LIST_REGIONS_OPERATION)
                .with_identifier("RunMat:GeometryListRegionsFailed")
                .build()
        })?;
    Ok(envelope.data)
}

pub fn geometry_query_entities_op(
    asset: &GeometryAsset,
    query: GeometryEntityQuery,
    context: OperationContext,
) -> Result<OperationEnvelope<GeometryEntityQueryResult>, OperationErrorEnvelope> {
    let requested_limit = query.limit.unwrap_or(DEFAULT_QUERY_LIMIT);
    if requested_limit == 0 {
        return Err(operation_error(
            GEOMETRY_QUERY_ENTITIES_OPERATION,
            GEOMETRY_QUERY_ENTITIES_OP_VERSION,
            &context,
            OperationErrorSpec {
                error_code: "RM.GEOMETRY.QUERY_ENTITIES.INVALID_LIMIT",
                error_type: OperationErrorType::Input,
                retryable: false,
                severity: OperationErrorSeverity::Error,
            },
            "entity query limit must be greater than zero",
            BTreeMap::new(),
        ));
    }

    if let Some(region_id) = query.region_id.as_ref() {
        find_region(asset, region_id)
            .map_err(|error| map_geometry_query_error(region_id, error, &context))?;
        return Ok(OperationEnvelope::new(
            GEOMETRY_QUERY_ENTITIES_OPERATION,
            GEOMETRY_QUERY_ENTITIES_OP_VERSION,
            &context,
            query_region_entities(asset, &query, region_id, requested_limit),
        ));
    }

    let mut entities = Vec::new();
    let mut produced_total = 0usize;

    for mesh in &asset.meshes {
        if query
            .mesh_id
            .as_ref()
            .is_some_and(|mesh_id| mesh_id != &mesh.mesh_id)
        {
            continue;
        }

        let count = match query.entity_kind {
            EntityKind::Node => mesh.vertex_count as usize,
            EntityKind::Element | EntityKind::Face => mesh.element_count as usize,
            EntityKind::Edge => 0,
        };

        produced_total += count;

        if entities.len() >= requested_limit {
            continue;
        }

        let remaining = requested_limit - entities.len();
        let emit = count.min(remaining);
        for entity_id in 0..emit {
            entities.push(EntityRef {
                geometry_id: asset.geometry_id.clone(),
                geometry_revision: asset.revision,
                mesh_id: mesh.mesh_id.clone(),
                entity_kind: query.entity_kind,
                entity_id: entity_id as u64,
            });
        }
    }

    Ok(OperationEnvelope::new(
        GEOMETRY_QUERY_ENTITIES_OPERATION,
        GEOMETRY_QUERY_ENTITIES_OP_VERSION,
        &context,
        GeometryEntityQueryResult {
            entities,
            truncated: produced_total > requested_limit,
        },
    ))
}

fn query_region_entities(
    asset: &GeometryAsset,
    query: &GeometryEntityQuery,
    region_id: &str,
    requested_limit: usize,
) -> GeometryEntityQueryResult {
    if query.entity_kind == EntityKind::Node {
        return query_region_nodes(asset, query, region_id, requested_limit);
    }

    let mut entities = Vec::new();
    let mut produced_total = 0usize;
    for mapping in asset.region_entity_mappings.iter().filter(|mapping| {
        mapping.region_id == region_id
            && query
                .mesh_id
                .as_ref()
                .is_none_or(|mesh_id| mesh_id == &mapping.mesh_id)
            && mapping_matches_query_kind(mapping.entity_kind, query.entity_kind)
    }) {
        let mapped_total = mapping.entity_count() as usize;
        produced_total = produced_total.saturating_add(mapped_total);
        if entities.len() >= requested_limit {
            continue;
        }
        for range in &mapping.ranges {
            let Some(end) = range.end_exclusive() else {
                continue;
            };
            for entity_id in range.start..end {
                if entities.len() >= requested_limit {
                    break;
                }
                entities.push(EntityRef {
                    geometry_id: asset.geometry_id.clone(),
                    geometry_revision: asset.revision,
                    mesh_id: mapping.mesh_id.clone(),
                    entity_kind: query.entity_kind,
                    entity_id,
                });
            }
        }
    }

    GeometryEntityQueryResult {
        entities,
        truncated: produced_total > requested_limit,
    }
}

fn query_region_nodes(
    asset: &GeometryAsset,
    query: &GeometryEntityQuery,
    region_id: &str,
    requested_limit: usize,
) -> GeometryEntityQueryResult {
    let mut node_refs = BTreeSet::<(String, u64)>::new();
    let mut truncated = false;

    for mapping in asset.region_entity_mappings.iter().filter(|mapping| {
        mapping.region_id == region_id
            && query
                .mesh_id
                .as_ref()
                .is_none_or(|mesh_id| mesh_id == &mapping.mesh_id)
            && mapping_matches_query_kind(mapping.entity_kind, EntityKind::Face)
    }) {
        let Some(surface_mesh) = asset
            .surface_meshes
            .iter()
            .find(|mesh| mesh.mesh_id == mapping.mesh_id)
        else {
            continue;
        };
        for range in &mapping.ranges {
            let Some(end) = range.end_exclusive() else {
                continue;
            };
            for face_id in range.start..end {
                let Some(triangle) = surface_mesh.triangles.get(face_id as usize) else {
                    continue;
                };
                for vertex_id in triangle {
                    node_refs.insert((mapping.mesh_id.clone(), *vertex_id as u64));
                    if node_refs.len() > requested_limit {
                        truncated = true;
                        break;
                    }
                }
                if truncated {
                    break;
                }
            }
            if truncated {
                break;
            }
        }
        if truncated {
            break;
        }
    }

    let entities = node_refs
        .into_iter()
        .take(requested_limit)
        .map(|(mesh_id, entity_id)| EntityRef {
            geometry_id: asset.geometry_id.clone(),
            geometry_revision: asset.revision,
            mesh_id,
            entity_kind: EntityKind::Node,
            entity_id,
        })
        .collect();

    GeometryEntityQueryResult {
        entities,
        truncated,
    }
}

fn mapping_matches_query_kind(mapping_kind: EntityKind, query_kind: EntityKind) -> bool {
    mapping_kind == query_kind
        || matches!(
            (mapping_kind, query_kind),
            (EntityKind::Face, EntityKind::Element) | (EntityKind::Element, EntityKind::Face)
        )
}

pub fn geometry_query_entities(
    asset: &GeometryAsset,
    query: GeometryEntityQuery,
) -> BuiltinResult<GeometryEntityQueryResult> {
    let envelope = geometry_query_entities_op(asset, query, OperationContext::new(None, None))
        .map_err(|error| {
            build_runtime_error(error.message)
                .with_builtin(GEOMETRY_QUERY_ENTITIES_OPERATION)
                .with_identifier("RunMat:GeometryQueryEntitiesFailed")
                .build()
        })?;
    Ok(envelope.data)
}

pub fn geometry_capture_view_op(
    asset: &GeometryAsset,
    view_spec: GeometryCaptureViewSpec,
    context: OperationContext,
) -> Result<OperationEnvelope<GeometryCaptureViewResult>, OperationErrorEnvelope> {
    if view_spec.width == 0 || view_spec.height == 0 {
        return Err(operation_error(
            GEOMETRY_CAPTURE_VIEW_OPERATION,
            GEOMETRY_CAPTURE_VIEW_OP_VERSION,
            &context,
            OperationErrorSpec {
                error_code: "RM.GEOMETRY.CAPTURE_VIEW.INVALID_SPEC",
                error_type: OperationErrorType::Input,
                retryable: false,
                severity: OperationErrorSeverity::Error,
            },
            "capture view dimensions must be greater than zero",
            BTreeMap::from([
                ("width".to_string(), view_spec.width.to_string()),
                ("height".to_string(), view_spec.height.to_string()),
            ]),
        ));
    }

    let adapter = GEOMETRY_CAPTURE_ADAPTER.with(|slot| *slot.borrow());
    if let Some(adapter) = adapter {
        let capture = adapter.capture(asset, &view_spec).map_err(|message| {
            operation_error(
                GEOMETRY_CAPTURE_VIEW_OPERATION,
                GEOMETRY_CAPTURE_VIEW_OP_VERSION,
                &context,
                OperationErrorSpec {
                    error_code: "RM.GEOMETRY.CAPTURE_VIEW.BACKEND_FAILED",
                    error_type: OperationErrorType::Backend,
                    retryable: true,
                    severity: OperationErrorSeverity::Error,
                },
                message,
                BTreeMap::from([
                    ("geometry_id".to_string(), asset.geometry_id.clone()),
                    ("adapter".to_string(), adapter.adapter_name().to_string()),
                ]),
            )
        })?;
        return Ok(OperationEnvelope::new(
            GEOMETRY_CAPTURE_VIEW_OPERATION,
            GEOMETRY_CAPTURE_VIEW_OP_VERSION,
            &context,
            capture,
        ));
    }

    if view_spec.format.eq_ignore_ascii_case("svg") {
        let capture = DEFAULT_SVG_CAPTURE_ADAPTER
            .capture(asset, &view_spec)
            .map_err(|message| {
                operation_error(
                    GEOMETRY_CAPTURE_VIEW_OPERATION,
                    GEOMETRY_CAPTURE_VIEW_OP_VERSION,
                    &context,
                    OperationErrorSpec {
                        error_code: "RM.GEOMETRY.CAPTURE_VIEW.BACKEND_FAILED",
                        error_type: OperationErrorType::Backend,
                        retryable: true,
                        severity: OperationErrorSeverity::Error,
                    },
                    message,
                    BTreeMap::from([
                        ("geometry_id".to_string(), asset.geometry_id.clone()),
                        (
                            "adapter".to_string(),
                            DEFAULT_SVG_CAPTURE_ADAPTER.adapter_name().to_string(),
                        ),
                    ]),
                )
            })?;
        return Ok(OperationEnvelope::new(
            GEOMETRY_CAPTURE_VIEW_OPERATION,
            GEOMETRY_CAPTURE_VIEW_OP_VERSION,
            &context,
            capture,
        ));
    }

    Err(operation_error(
        GEOMETRY_CAPTURE_VIEW_OPERATION,
        GEOMETRY_CAPTURE_VIEW_OP_VERSION,
        &context,
        OperationErrorSpec {
            error_code: "RM.GEOMETRY.CAPTURE_VIEW.UNSUPPORTED",
            error_type: OperationErrorType::Backend,
            retryable: false,
            severity: OperationErrorSeverity::Error,
        },
        "geometry view capture is not wired in runtime yet",
        BTreeMap::from([("geometry_id".to_string(), asset.geometry_id.clone())]),
    ))
}

pub fn geometry_capture_view(
    asset: &GeometryAsset,
    view_spec: GeometryCaptureViewSpec,
) -> BuiltinResult<GeometryCaptureViewResult> {
    let envelope = geometry_capture_view_op(asset, view_spec, OperationContext::new(None, None))
        .map_err(|error| {
            build_runtime_error(error.message)
                .with_builtin(GEOMETRY_CAPTURE_VIEW_OPERATION)
                .with_identifier("RunMat:GeometryCaptureViewFailed")
                .build()
        })?;
    Ok(envelope.data)
}

#[cfg(feature = "plot-core")]
pub fn geometry_preview_scene(
    asset: &GeometryAsset,
    title: impl Into<String>,
    options: GeometryPreviewSceneOptions,
) -> Result<runmat_plot::GeometryScene, String> {
    if asset.surface_meshes.is_empty() {
        return Err("geometry asset does not contain renderable surface mesh data".to_string());
    }

    let triangles_per_chunk = options.triangles_per_chunk.max(1);
    let cad_presentation = options.presentation == GeometryPreviewPresentation::Cad;
    let mut chunks = Vec::new();

    for (mesh_index, surface_mesh) in asset.surface_meshes.iter().enumerate() {
        if surface_mesh.vertices.is_empty() || surface_mesh.triangles.is_empty() {
            continue;
        }
        let positions = surface_mesh
            .vertices
            .iter()
            .map(|position| {
                Ok([
                    f64_to_f32_coordinate(position[0])?,
                    f64_to_f32_coordinate(position[1])?,
                    f64_to_f32_coordinate(position[2])?,
                ])
            })
            .collect::<Result<Vec<_>, String>>()?;

        let presentation = cad_presentation.then(|| {
            cad_mesh_presentation(
                asset,
                &surface_mesh.mesh_id,
                surface_mesh.triangles.len(),
                surface_mesh.vertices.len(),
            )
        });
        let base_color = if cad_presentation {
            CAD_DEFAULT_FACE_COLOR
        } else {
            preview_mesh_color(mesh_index)
        };
        let alpha = if options.xray { 0.34 } else { base_color.w };
        let mut material = runmat_plot::cad_default_material();
        material.albedo = glam::Vec4::new(base_color.x, base_color.y, base_color.z, alpha);
        if options.xray {
            material.alpha_mode = runmat_plot::core::AlphaMode::Blend;
        }

        let mut chunk_index = 0usize;
        let mut chunk_start_triangle = 0usize;
        while chunk_start_triangle < surface_mesh.triangles.len() {
            let owner_node_ids = presentation
                .as_ref()
                .map(|item| {
                    item.owner_node_ids_for_triangle(chunk_start_triangle)
                        .to_vec()
                })
                .unwrap_or_default();
            let mut chunk_end_triangle = chunk_start_triangle + 1;
            while chunk_end_triangle < surface_mesh.triangles.len()
                && chunk_end_triangle.saturating_sub(chunk_start_triangle) < triangles_per_chunk
            {
                let next_owner_node_ids = presentation
                    .as_ref()
                    .map(|item| item.owner_node_ids_for_triangle(chunk_end_triangle))
                    .unwrap_or(&[]);
                if next_owner_node_ids != owner_node_ids.as_slice() {
                    break;
                }
                chunk_end_triangle += 1;
            }
            let triangles = &surface_mesh.triangles[chunk_start_triangle..chunk_end_triangle];
            let mut remap = HashMap::<u32, u32>::with_capacity(triangles.len() * 3);
            let mut local_positions = Vec::<[f32; 3]>::new();
            let mut local_colors = Vec::<[f32; 4]>::new();
            let mut indices = Vec::<u32>::with_capacity(triangles.len() * 3);

            for triangle in triangles {
                for source_index in triangle {
                    let local_index = if let Some(local_index) = remap.get(source_index) {
                        *local_index
                    } else {
                        let source_index_usize = usize::try_from(*source_index).map_err(|_| {
                            format!(
                                "surface mesh '{}' has an invalid vertex index",
                                surface_mesh.mesh_id
                            )
                        })?;
                        let position = positions.get(source_index_usize).ok_or_else(|| {
                            format!(
                                "surface mesh '{}' references vertex {} outside {} vertices",
                                surface_mesh.mesh_id,
                                source_index,
                                positions.len()
                            )
                        })?;
                        let local_index = u32::try_from(local_positions.len()).map_err(|_| {
                            format!(
                                "surface mesh '{}' preview chunk exceeded u32 vertex indices",
                                surface_mesh.mesh_id
                            )
                        })?;
                        local_positions.push(*position);
                        let vertex_color = presentation
                            .as_ref()
                            .and_then(|item| item.vertex_colors.as_ref())
                            .and_then(|colors| colors.get(source_index_usize))
                            .copied()
                            .unwrap_or(base_color);
                        local_colors.push([vertex_color.x, vertex_color.y, vertex_color.z, alpha]);
                        remap.insert(*source_index, local_index);
                        local_index
                    };
                    indices.push(local_index);
                }
            }

            let normals = local_vertex_normals(&local_positions, &indices);
            let vertices = local_positions
                .into_iter()
                .zip(local_colors)
                .zip(normals)
                .map(|((position, color), normal)| {
                    runmat_plot::geometry_scene_vertex(position, color, normal)
                })
                .collect::<Vec<_>>();
            let regions = geometry_scene_regions_for_surface_chunk(
                asset,
                &surface_mesh.mesh_id,
                chunk_start_triangle,
                triangles.len(),
            );
            let chunk = runmat_plot::GeometrySceneChunk::indexed_triangles(
                format!("{}:chunk_{chunk_index}", surface_mesh.mesh_id),
                vertices,
                indices,
                material.clone(),
            )
            .with_mesh_id(surface_mesh.mesh_id.clone())
            .with_label(format!(
                "{} chunk {}",
                surface_mesh.mesh_id,
                chunk_index + 1
            ))
            .with_regions(regions)
            .with_owner_node_ids(owner_node_ids);
            chunks.push(chunk);
            chunk_start_triangle = chunk_end_triangle;
            chunk_index += 1;
        }
    }

    if chunks.is_empty() {
        return Err("geometry asset did not contain renderable surface mesh triangles".to_string());
    }

    Ok(
        runmat_plot::GeometryScene::new(geometry_scene_id(asset), asset.revision as u64, chunks)
            .with_title(title),
    )
}

#[cfg(feature = "plot-core")]
pub fn geometry_preview_scene_overlay(
    asset: &GeometryAsset,
    source_name: Option<String>,
    status: runmat_plot::GeometrySceneCompleteness,
    quality_label: impl Into<String>,
    format: Option<String>,
    byte_count: Option<u64>,
    allow_create_fea_study: bool,
) -> runmat_plot::GeometrySceneOverlay {
    let mapping_summary = region_mapping_summary(asset, DEFAULT_MAPPING_RANGE_PREVIEW_LIMIT);
    let source_label = Some(format!(
        "{} / {}",
        source_geometry_kind_label(asset.source_geometry.kind),
        asset.source.importer_version
    ));
    let warnings = asset
        .diagnostics
        .iter()
        .filter(|diagnostic| {
            matches!(
                diagnostic.severity,
                DiagnosticSeverity::Warning | DiagnosticSeverity::Error
            )
        })
        .take(4)
        .map(|diagnostic| diagnostic.message.clone())
        .collect();

    runmat_plot::GeometrySceneOverlay {
        source_name,
        status,
        quality_label: Some(quality_label.into()),
        format,
        source_label,
        allow_create_fea_study,
        byte_count,
        mesh_count: asset.meshes.len(),
        vertex_count: asset
            .surface_meshes
            .iter()
            .map(|mesh| mesh.vertices.len())
            .sum(),
        triangle_count: asset
            .surface_meshes
            .iter()
            .map(|mesh| mesh.triangles.len())
            .sum(),
        progress_percent: None,
        region_count: asset.regions.len(),
        mapped_region_count: mapping_summary.mapped_region_count,
        assembly_nodes: asset
            .source_geometry
            .assembly
            .as_ref()
            .map(|node| vec![geometry_scene_assembly_node(node)])
            .unwrap_or_default(),
        regions: geometry_scene_region_summaries(asset),
        warnings,
    }
}

#[cfg(feature = "plot-core")]
fn geometry_scene_assembly_node(node: &AssemblyNode) -> runmat_plot::GeometrySceneAssemblyNode {
    runmat_plot::GeometrySceneAssemblyNode {
        node_id: node.node_id.clone(),
        label: node.label.clone(),
        children: node
            .children
            .iter()
            .map(geometry_scene_assembly_node)
            .collect(),
    }
}

#[cfg(feature = "plot-core")]
fn geometry_scene_region_summaries(
    asset: &GeometryAsset,
) -> Vec<runmat_plot::GeometrySceneRegionSummary> {
    let mut triangle_counts: BTreeMap<String, usize> = BTreeMap::new();
    for mapping in &asset.region_entity_mappings {
        if !matches!(mapping.entity_kind, EntityKind::Face | EntityKind::Element) {
            continue;
        }
        let count = mapping
            .ranges
            .iter()
            .filter_map(|range| {
                range
                    .end_exclusive()
                    .map(|end| end.saturating_sub(range.start))
            })
            .map(|count| usize::try_from(count).unwrap_or(usize::MAX))
            .fold(0usize, |total, count| total.saturating_add(count));
        triangle_counts
            .entry(mapping.region_id.clone())
            .and_modify(|total| *total = total.saturating_add(count))
            .or_insert(count);
    }

    asset
        .regions
        .iter()
        .map(|region| runmat_plot::GeometrySceneRegionSummary {
            region_id: region.region_id.clone(),
            label: region.name.clone(),
            tag: region.tag.clone(),
            kind: region
                .cad_ownership
                .as_ref()
                .and_then(|ownership| ownership.label.as_ref())
                .map(|label| format!("{:?}", label.kind).to_ascii_lowercase()),
            triangle_count: triangle_counts
                .get(&region.region_id)
                .copied()
                .unwrap_or_default(),
        })
        .collect()
}

#[cfg(feature = "plot-core")]
fn source_geometry_kind_label(kind: SourceGeometryKind) -> &'static str {
    match kind {
        SourceGeometryKind::Mesh => "mesh",
        SourceGeometryKind::Cad => "cad",
    }
}

#[cfg(feature = "plot-core")]
pub fn geometry_preview_figure(
    asset: &GeometryAsset,
    title: impl Into<String>,
    options: GeometryPreviewFigureOptions,
) -> Result<runmat_plot::plots::Figure, String> {
    if asset.surface_meshes.is_empty() {
        return Err("geometry asset does not contain renderable surface mesh data".to_string());
    }

    let cad_presentation = options.presentation == GeometryPreviewPresentation::Cad;
    let mut figure = if cad_presentation {
        runmat_plot::plots::Figure::new()
            .with_grid(false)
            .with_legend(false)
            .with_axis_equal(true)
    } else {
        let mut figure = runmat_plot::plots::Figure::new()
            .with_title(title)
            .with_labels("X", "Y")
            .with_grid(true)
            .with_axis_equal(true);
        figure.z_label = Some("Z".to_string());
        figure
    };
    if cad_presentation {
        figure.set_axes_view(0, -38.0, 24.0);
    }

    for (index, surface_mesh) in asset.surface_meshes.iter().enumerate() {
        let vertices = surface_mesh
            .vertices
            .iter()
            .map(|vertex| {
                Ok(glam::Vec3::new(
                    f64_to_f32_coordinate(vertex[0])?,
                    f64_to_f32_coordinate(vertex[1])?,
                    f64_to_f32_coordinate(vertex[2])?,
                ))
            })
            .collect::<Result<Vec<_>, String>>()?;
        let mut mesh = runmat_plot::plots::MeshPlot::new(vertices, surface_mesh.triangles.clone())?;
        mesh.set_mesh_id(Some(surface_mesh.mesh_id.clone()));
        mesh.set_regions(mesh_regions_for_surface(asset, &surface_mesh.mesh_id));
        if !cad_presentation {
            mesh.set_label(Some(format!(
                "{}: {} triangles",
                surface_mesh.mesh_id,
                surface_mesh.triangles.len()
            )));
        }

        if cad_presentation {
            let presentation = cad_mesh_presentation(
                asset,
                &surface_mesh.mesh_id,
                surface_mesh.triangles.len(),
                surface_mesh.vertices.len(),
            );
            mesh.set_face_color(CAD_DEFAULT_FACE_COLOR);
            mesh.set_edge_color(CAD_FEATURE_EDGE_COLOR);
            mesh.set_face_alpha(if options.xray { 0.34 } else { 1.0 });
            mesh.set_edge_alpha(if options.xray { 0.9 } else { 0.72 });
            if let Some(colors) = presentation.vertex_colors {
                mesh.set_vertex_colors(Some(colors))?;
            }
            if let Some(groups) = presentation.feature_edge_groups {
                mesh.set_feature_edge_groups(Some(groups))?;
                mesh.set_edge_mode(runmat_plot::plots::MeshEdgeMode::Feature);
                mesh.set_edge_width(0.85);
            } else if surface_mesh.triangles.len() > options.edge_overlay_triangle_limit {
                mesh.set_edge_mode(runmat_plot::plots::MeshEdgeMode::None);
                mesh.set_edge_width(0.0);
            } else {
                mesh.set_edge_mode(runmat_plot::plots::MeshEdgeMode::All);
                mesh.set_edge_width(0.28);
            }
        } else {
            let color = preview_mesh_color(index);
            mesh.set_face_color(color);
            mesh.set_edge_color(glam::Vec4::new(0.86, 0.91, 1.0, 0.82));
            mesh.set_face_alpha(0.92);
            if surface_mesh.triangles.len() > options.edge_overlay_triangle_limit {
                mesh.set_edge_width(0.0);
            } else {
                mesh.set_edge_width(0.35);
            }
        }
        figure.add_mesh_plot(mesh);
    }

    Ok(figure)
}

#[cfg(feature = "plot-core")]
#[derive(Debug, Default)]
struct CadMeshPresentation {
    feature_edge_groups: Option<Vec<u64>>,
    vertex_colors: Option<Vec<glam::Vec4>>,
    owner_paths: Vec<Vec<String>>,
    triangle_owner_path_indices: Option<Vec<Option<usize>>>,
}

#[cfg(feature = "plot-core")]
impl CadMeshPresentation {
    fn owner_node_ids_for_triangle(&self, triangle_index: usize) -> &[String] {
        self.triangle_owner_path_indices
            .as_ref()
            .and_then(|indices| indices.get(triangle_index))
            .and_then(|index| index.and_then(|index| self.owner_paths.get(index)))
            .map(Vec::as_slice)
            .unwrap_or(&[])
    }
}

#[cfg(feature = "plot-core")]
fn cad_mesh_presentation(
    asset: &GeometryAsset,
    mesh_id: &str,
    triangle_count: usize,
    vertex_count: usize,
) -> CadMeshPresentation {
    if triangle_count == 0 {
        return CadMeshPresentation::default();
    }

    let prefer_face_mappings = asset.source_geometry.kind == SourceGeometryKind::Cad;
    let mut feature_edge_groups = vec![0_u64; triangle_count];
    let mut vertex_colors = vec![CAD_DEFAULT_FACE_COLOR; vertex_count];
    let mut triangle_owner_path_indices = vec![None; triangle_count];
    let mut owner_paths = Vec::<Vec<String>>::new();
    let mut owner_path_indices = BTreeMap::<Vec<String>, usize>::new();
    let mut group_ids_by_region = BTreeMap::<String, u64>::new();
    let mut assigned_groups = false;
    let mut assigned_colors = false;
    let mut assigned_owner_paths = false;
    let surface_triangles = asset
        .surface_meshes
        .iter()
        .find(|surface_mesh| surface_mesh.mesh_id == mesh_id)
        .map(|surface_mesh| surface_mesh.triangles.as_slice());

    for mapping in asset.region_entity_mappings.iter().filter(|mapping| {
        mapping.mesh_id == mesh_id
            && matches!(mapping.entity_kind, EntityKind::Face | EntityKind::Element)
    }) {
        let Some(region) = asset
            .regions
            .iter()
            .find(|region| region.region_id == mapping.region_id)
        else {
            continue;
        };
        let face_id = region
            .cad_ownership
            .as_ref()
            .and_then(|ownership| ownership.face_id);
        if prefer_face_mappings && face_id.is_none() {
            continue;
        }
        let group_id = face_id
            .map(|face_id| face_id.saturating_add(1))
            .unwrap_or_else(|| {
                if let Some(group_id) = group_ids_by_region.get(&mapping.region_id) {
                    *group_id
                } else {
                    let group_id = group_ids_by_region.len() as u64 + 1;
                    group_ids_by_region.insert(mapping.region_id.clone(), group_id);
                    group_id
                }
            });
        let color = cad_region_color(region);
        let owner_node_ids = cad_region_owner_node_ids(region);
        let owner_path_index = if owner_node_ids.is_empty() {
            None
        } else if let Some(index) = owner_path_indices.get(&owner_node_ids) {
            Some(*index)
        } else {
            let index = owner_paths.len();
            owner_path_indices.insert(owner_node_ids.clone(), index);
            owner_paths.push(owner_node_ids);
            Some(index)
        };
        for range in &mapping.ranges {
            for triangle_index in bounded_range(range, triangle_count) {
                feature_edge_groups[triangle_index] = group_id;
                assigned_groups = true;
                if let Some(owner_path_index) = owner_path_index {
                    triangle_owner_path_indices[triangle_index] = Some(owner_path_index);
                    assigned_owner_paths = true;
                }
                if let Some(color) = color {
                    assigned_colors |= color_vertices_for_triangle(
                        surface_triangles,
                        triangle_index,
                        color,
                        &mut vertex_colors,
                    );
                }
            }
        }
    }

    CadMeshPresentation {
        feature_edge_groups: assigned_groups.then_some(feature_edge_groups),
        vertex_colors: assigned_colors.then_some(vertex_colors),
        owner_paths,
        triangle_owner_path_indices: assigned_owner_paths.then_some(triangle_owner_path_indices),
    }
}

#[cfg(feature = "plot-core")]
fn cad_region_owner_node_ids(region: &Region) -> Vec<String> {
    let Some(ownership) = region.cad_ownership.as_ref() else {
        return Vec::new();
    };
    let mut ids = Vec::new();
    // Render chunk ownership drives assembly-tree visibility. Face labels remain
    // region metadata so picking/selectors can stay face-level without forcing
    // one GPU draw item per face.
    for owner in &ownership.owner_path {
        push_unique_owner_node_id(&mut ids, &owner.label_entry);
    }
    ids
}

#[cfg(feature = "plot-core")]
fn push_unique_owner_node_id(ids: &mut Vec<String>, candidate: &str) {
    let candidate = candidate.trim();
    if candidate.is_empty() || ids.iter().any(|existing| existing == candidate) {
        return;
    }
    ids.push(candidate.to_string());
}

#[cfg(feature = "plot-core")]
fn bounded_range(range: &EntityIdRange, upper_bound: usize) -> std::ops::Range<usize> {
    let start = usize::try_from(range.start).unwrap_or(usize::MAX);
    let count = usize::try_from(range.count).unwrap_or(usize::MAX);
    let start = start.min(upper_bound);
    let end = start.saturating_add(count).min(upper_bound);
    start..end
}

#[cfg(feature = "plot-core")]
fn color_vertices_for_triangle(
    triangles: Option<&[[u32; 3]]>,
    triangle_index: usize,
    color: glam::Vec4,
    vertex_colors: &mut [glam::Vec4],
) -> bool {
    let Some(triangle) = triangles.and_then(|triangles| triangles.get(triangle_index)) else {
        return false;
    };
    let mut colored = false;
    for vertex_id in triangle {
        if let Some(slot) = vertex_colors.get_mut(*vertex_id as usize) {
            *slot = color;
            colored = true;
        }
    }
    colored
}

#[cfg(feature = "plot-core")]
fn cad_region_color(region: &Region) -> Option<glam::Vec4> {
    region
        .cad_ownership
        .as_ref()
        .and_then(|ownership| ownership.color.as_ref())
        .and_then(|color| parse_cad_hex_rgba(&color.hex_rgba))
        .map(cad_display_color)
}

#[cfg(feature = "plot-core")]
fn parse_cad_hex_rgba(value: &str) -> Option<glam::Vec4> {
    let value = value.trim().trim_start_matches('#');
    if value.len() != 6 && value.len() != 8 {
        return None;
    }
    let r = u8::from_str_radix(&value[0..2], 16).ok()? as f32 / 255.0;
    let g = u8::from_str_radix(&value[2..4], 16).ok()? as f32 / 255.0;
    let b = u8::from_str_radix(&value[4..6], 16).ok()? as f32 / 255.0;
    let a = if value.len() == 8 {
        u8::from_str_radix(&value[6..8], 16).ok()? as f32 / 255.0
    } else {
        1.0
    };
    Some(glam::Vec4::new(r, g, b, a))
}

#[cfg(feature = "plot-core")]
fn cad_display_color(color: glam::Vec4) -> glam::Vec4 {
    let rgb = glam::Vec3::new(color.x, color.y, color.z);
    let gray = glam::Vec3::splat((rgb.x + rgb.y + rgb.z) / 3.0);
    let softened = rgb
        .lerp(gray, 0.18)
        .lerp(CAD_DEFAULT_FACE_COLOR.truncate(), 0.16);
    glam::Vec4::new(softened.x, softened.y, softened.z, color.w.max(0.2))
}

#[cfg(feature = "plot-core")]
fn mesh_regions_for_surface(
    asset: &GeometryAsset,
    mesh_id: &str,
) -> Vec<runmat_plot::plots::MeshRegion> {
    asset
        .region_entity_mappings
        .iter()
        .filter(|mapping| {
            mapping.mesh_id == mesh_id
                && matches!(mapping.entity_kind, EntityKind::Face | EntityKind::Element)
        })
        .filter_map(|mapping| {
            let triangle_ranges = mapping
                .ranges
                .iter()
                .filter_map(|range| {
                    let start = u32::try_from(range.start).ok()?;
                    let count = u32::try_from(range.count).ok()?;
                    if count == 0 {
                        None
                    } else {
                        Some(runmat_plot::plots::MeshTriangleRange::new(start, count))
                    }
                })
                .collect::<Vec<_>>();
            if triangle_ranges.is_empty() {
                return None;
            }
            let region = asset
                .regions
                .iter()
                .find(|region| region.region_id == mapping.region_id);
            Some(runmat_plot::plots::MeshRegion::new(
                mapping.region_id.clone(),
                region.map(|region| region.name.clone()),
                region.and_then(|region| region.tag.clone()),
                triangle_ranges,
            ))
        })
        .collect()
}

#[cfg(feature = "plot-core")]
fn geometry_scene_id(asset: &GeometryAsset) -> String {
    format!(
        "{}:{}:{}",
        asset.geometry_id, asset.source.sha256, asset.tessellation_profile.profile_id
    )
}

#[cfg(feature = "plot-core")]
fn geometry_scene_regions_for_surface_chunk(
    asset: &GeometryAsset,
    mesh_id: &str,
    chunk_start_triangle: usize,
    chunk_triangle_count: usize,
) -> Vec<runmat_plot::GeometrySceneRegion> {
    if chunk_triangle_count == 0 {
        return Vec::new();
    }
    let chunk_start = chunk_start_triangle as u64;
    let chunk_end = chunk_start.saturating_add(chunk_triangle_count as u64);
    asset
        .region_entity_mappings
        .iter()
        .filter(|mapping| {
            mapping.mesh_id == mesh_id
                && matches!(mapping.entity_kind, EntityKind::Face | EntityKind::Element)
        })
        .filter_map(|mapping| {
            let triangle_ranges = mapping
                .ranges
                .iter()
                .filter_map(|range| {
                    let range_end = range.end_exclusive()?;
                    let start = range.start.max(chunk_start);
                    let end = range_end.min(chunk_end);
                    if end <= start {
                        return None;
                    }
                    let local_start = u32::try_from(start - chunk_start).ok()?;
                    let count = u32::try_from(end - start).ok()?;
                    Some(runmat_plot::GeometrySceneTriangleRange::new(
                        local_start,
                        count,
                    ))
                })
                .collect::<Vec<_>>();
            if triangle_ranges.is_empty() {
                return None;
            }
            let region = asset
                .regions
                .iter()
                .find(|region| region.region_id == mapping.region_id);
            Some(runmat_plot::GeometrySceneRegion::new(
                mapping.region_id.clone(),
                region.map(|region| region.name.clone()),
                region.and_then(|region| region.tag.clone()),
                triangle_ranges,
            ))
        })
        .collect()
}

#[cfg(feature = "plot-core")]
fn local_vertex_normals(positions: &[[f32; 3]], indices: &[u32]) -> Vec<[f32; 3]> {
    let mut normals = vec![[0.0, 0.0, 0.0]; positions.len()];
    for triangle in indices.chunks_exact(3) {
        let a = triangle[0] as usize;
        let b = triangle[1] as usize;
        let c = triangle[2] as usize;
        if a >= positions.len() || b >= positions.len() || c >= positions.len() {
            continue;
        }
        let normal = face_normal(positions[a], positions[b], positions[c]);
        accumulate_normal(&mut normals[a], normal);
        accumulate_normal(&mut normals[b], normal);
        accumulate_normal(&mut normals[c], normal);
    }
    normals.into_iter().map(normalize_or_default).collect()
}

#[cfg(feature = "plot-core")]
fn face_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
    let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
    let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
    normalize_or_default([
        ab[1] * ac[2] - ab[2] * ac[1],
        ab[2] * ac[0] - ab[0] * ac[2],
        ab[0] * ac[1] - ab[1] * ac[0],
    ])
}

#[cfg(feature = "plot-core")]
fn accumulate_normal(target: &mut [f32; 3], normal: [f32; 3]) {
    target[0] += normal[0];
    target[1] += normal[1];
    target[2] += normal[2];
}

#[cfg(feature = "plot-core")]
fn normalize_or_default(value: [f32; 3]) -> [f32; 3] {
    let length_squared = value[0] * value[0] + value[1] * value[1] + value[2] * value[2];
    if length_squared <= f32::EPSILON || !length_squared.is_finite() {
        return [0.0, 0.0, 1.0];
    }
    let inv_length = length_squared.sqrt().recip();
    [
        value[0] * inv_length,
        value[1] * inv_length,
        value[2] * inv_length,
    ]
}

#[cfg(feature = "plot-core")]
fn f64_to_f32_coordinate(value: f64) -> Result<f32, String> {
    if !value.is_finite() {
        return Err("geometry preview mesh contains a non-finite coordinate".to_string());
    }
    if value < f32::MIN as f64 || value > f32::MAX as f64 {
        return Err("geometry preview mesh coordinate exceeds f32 render range".to_string());
    }
    Ok(value as f32)
}

#[cfg(feature = "plot-core")]
fn preview_mesh_color(index: usize) -> glam::Vec4 {
    const PALETTE: [[f32; 4]; 6] = [
        [0.18, 0.48, 0.86, 1.0],
        [0.13, 0.62, 0.44, 1.0],
        [0.84, 0.43, 0.18, 1.0],
        [0.57, 0.38, 0.77, 1.0],
        [0.73, 0.62, 0.18, 1.0],
        [0.20, 0.62, 0.75, 1.0],
    ];
    glam::Vec4::from_array(PALETTE[index % PALETTE.len()])
}

pub fn geometry_prep_for_analysis_op(
    asset: &GeometryAsset,
    spec: GeometryPrepForAnalysisSpec,
    context: OperationContext,
) -> Result<OperationEnvelope<GeometryPrepForAnalysisResult>, OperationErrorEnvelope> {
    if spec.target_element_budget == 0 {
        return Err(operation_error(
            GEOMETRY_PREP_FOR_ANALYSIS_OPERATION,
            GEOMETRY_PREP_FOR_ANALYSIS_OP_VERSION,
            &context,
            OperationErrorSpec {
                error_code: "RM.GEOMETRY.PREP_FOR_ANALYSIS.INVALID_SPEC",
                error_type: OperationErrorType::Input,
                retryable: false,
                severity: OperationErrorSeverity::Error,
            },
            "prep-for-analysis target_element_budget must be greater than zero",
            BTreeMap::from([(
                "target_element_budget".to_string(),
                spec.target_element_budget.to_string(),
            )]),
        ));
    }

    let profile = match spec.profile {
        GeometryPrepProfile::SurfaceOnly => MeshingProfile::SurfaceOnly,
        GeometryPrepProfile::AnalysisReady => MeshingProfile::AnalysisReady,
        GeometryPrepProfile::AdaptiveRefine => MeshingProfile::AdaptiveRefine,
    };
    let prepared = prepare_geometry_for_analysis(
        asset,
        MeshingOptions {
            profile,
            target_element_budget: spec.target_element_budget,
        },
    )
    .map_err(|error| {
        operation_error(
            GEOMETRY_PREP_FOR_ANALYSIS_OPERATION,
            GEOMETRY_PREP_FOR_ANALYSIS_OP_VERSION,
            &context,
            OperationErrorSpec {
                error_code: "RM.GEOMETRY.PREP_FOR_ANALYSIS.FAILED",
                error_type: OperationErrorType::Validation,
                retryable: false,
                severity: OperationErrorSeverity::Error,
            },
            format!("failed to prepare geometry for analysis: {error}"),
            BTreeMap::from([("geometry_id".to_string(), asset.geometry_id.clone())]),
        )
    })?;

    let artifact = persist_prep_artifact(asset, prepared).map_err(|error| {
        operation_error(
            GEOMETRY_PREP_FOR_ANALYSIS_OPERATION,
            GEOMETRY_PREP_FOR_ANALYSIS_OP_VERSION,
            &context,
            OperationErrorSpec {
                error_code: "RM.GEOMETRY.PREP_FOR_ANALYSIS.ARTIFACT_STORE_FAILED",
                error_type: OperationErrorType::Internal,
                retryable: true,
                severity: OperationErrorSeverity::Error,
            },
            format!("failed to persist prep artifact: {error}"),
            BTreeMap::from([("geometry_id".to_string(), asset.geometry_id.clone())]),
        )
    })?;

    Ok(OperationEnvelope::new(
        GEOMETRY_PREP_FOR_ANALYSIS_OPERATION,
        GEOMETRY_PREP_FOR_ANALYSIS_OP_VERSION,
        &context,
        GeometryPrepForAnalysisResult {
            prep_artifact_id: artifact.prep_artifact_id,
            prep: artifact.prep,
        },
    ))
}

pub fn geometry_prep_for_analysis(
    asset: &GeometryAsset,
    spec: GeometryPrepForAnalysisSpec,
) -> BuiltinResult<GeometryPrepForAnalysisResult> {
    let envelope = geometry_prep_for_analysis_op(asset, spec, OperationContext::new(None, None))
        .map_err(|error| {
            build_runtime_error(error.message)
                .with_builtin(GEOMETRY_PREP_FOR_ANALYSIS_OPERATION)
                .with_identifier("RunMat:GeometryPrepForAnalysisFailed")
                .build()
        })?;
    Ok(envelope.data)
}

fn format_name(format: GeometryFormat) -> &'static str {
    match format {
        runmat_geometry_io::GeometryFormat::Stl => "stl",
        runmat_geometry_io::GeometryFormat::Step => "step",
        runmat_geometry_io::GeometryFormat::Iges => "iges",
        runmat_geometry_io::GeometryFormat::Brep => "brep",
        runmat_geometry_io::GeometryFormat::Obj => "obj",
        runmat_geometry_io::GeometryFormat::Ply => "ply",
        runmat_geometry_io::GeometryFormat::Gltf => "gltf",
        runmat_geometry_io::GeometryFormat::Unknown => "unknown",
    }
}

fn map_geometry_load_error(
    path: &str,
    error: GeometryImportError,
    context: &OperationContext,
) -> OperationErrorEnvelope {
    let (error_code, error_type, retryable) = match &error {
        GeometryImportError::UnsupportedFormat => (
            "RM.GEOMETRY.LOAD.FORMAT_UNSUPPORTED",
            OperationErrorType::Input,
            false,
        ),
        GeometryImportError::ParseFailed(_) => (
            "RM.GEOMETRY.LOAD.PARSE_FAILED",
            OperationErrorType::Validation,
            false,
        ),
        GeometryImportError::CapacityExceeded { .. } => (
            "RM.GEOMETRY.LOAD.CAPACITY_LIMIT_EXCEEDED",
            OperationErrorType::Capacity,
            false,
        ),
        GeometryImportError::BackendUnavailable(_) => (
            "RM.GEOMETRY.LOAD.BACKEND_UNAVAILABLE",
            OperationErrorType::Backend,
            false,
        ),
        GeometryImportError::Cancelled => (
            "RM.GEOMETRY.LOAD.CANCELLED",
            OperationErrorType::Cancelled,
            false,
        ),
    };
    operation_error(
        GEOMETRY_LOAD_OPERATION,
        GEOMETRY_LOAD_OP_VERSION,
        context,
        OperationErrorSpec {
            error_code,
            error_type,
            retryable,
            severity: OperationErrorSeverity::Error,
        },
        error.to_string(),
        BTreeMap::from([("path".to_string(), path.to_string())]),
    )
}

fn map_geometry_query_error(
    region_id: &str,
    error: QueryError,
    context: &OperationContext,
) -> OperationErrorEnvelope {
    match error {
        QueryError::RegionNotFound => operation_error(
            GEOMETRY_QUERY_ENTITIES_OPERATION,
            GEOMETRY_QUERY_ENTITIES_OP_VERSION,
            context,
            OperationErrorSpec {
                error_code: "RM.GEOMETRY.QUERY_ENTITIES.REGION_NOT_FOUND",
                error_type: OperationErrorType::Validation,
                retryable: false,
                severity: OperationErrorSeverity::Error,
            },
            format!("region '{region_id}' does not exist"),
            BTreeMap::from([("region_id".to_string(), region_id.to_string())]),
        ),
    }
}

#[cfg(test)]
mod tests;
