Skip to main content

json_eval_rs/jsoneval/
dependents.rs

1use super::JSONEval;
2use crate::jsoneval::cancellation::CancellationToken;
3use crate::jsoneval::json_parser;
4use crate::jsoneval::path_utils;
5use crate::jsoneval::path_utils::get_value_by_pointer_without_properties;
6use crate::jsoneval::path_utils::normalize_to_json_pointer;
7use crate::jsoneval::types::DependentItem;
8use crate::rlogic::{LogicId, RLogic};
9use crate::utils::clean_float_noise_scalar;
10use crate::EvalData;
11
12use indexmap::{IndexMap, IndexSet};
13use serde_json::Value;
14
15impl JSONEval {
16    /// Evaluate fields that depend on a changed path.
17    /// Processes all dependent fields transitively, then optionally performs a full
18    /// re-evaluation pass (for read-only / hide effects) and cascades into subforms.
19    pub fn evaluate_dependents(
20        &mut self,
21        changed_paths: &[String],
22        data: Option<&str>,
23        context: Option<&str>,
24        re_evaluate: bool,
25        token: Option<&CancellationToken>,
26        mut canceled_paths: Option<&mut Vec<String>>,
27        include_subforms: bool,
28    ) -> Result<Value, String> {
29        if let Some(t) = token {
30            if t.is_cancelled() {
31                return Err("Cancelled".to_string());
32            }
33        }
34        let _lock = self.eval_lock.lock().unwrap();
35
36        // Update data if provided, diff versions
37        if let Some(data_str) = data {
38            let data_value = json_parser::parse_json_str(data_str)?;
39            let context_value = if let Some(ctx) = context {
40                json_parser::parse_json_str(ctx)?
41            } else {
42                Value::Object(serde_json::Map::new())
43            };
44            let old_data = self.eval_data.snapshot_data_clone();
45            self.eval_data
46                .replace_data_and_context(data_value, context_value);
47            let new_data = self.eval_data.snapshot_data_clone();
48            self.eval_cache
49                .store_snapshot_and_diff_versions(&old_data, &new_data);
50        }
51
52        let mut result = Vec::new();
53        let mut processed = IndexSet::new();
54        let mut to_process: Vec<(String, bool)> = changed_paths
55            .iter()
56            .map(|path| (path_utils::dot_notation_to_schema_pointer(path), false))
57            .collect();
58
59        Self::process_dependents_queue(
60            &self.engine,
61            &self.evaluations,
62            &mut self.eval_data,
63            &mut self.eval_cache,
64            &self.dependents_evaluations,
65            &self.evaluated_schema,
66            &mut to_process,
67            &mut processed,
68            &mut result,
69            token,
70            canceled_paths.as_mut().map(|v| &mut **v),
71        )?;
72
73        // Drop the lock before calling sub-methods that may re-acquire it
74        drop(_lock);
75
76        if re_evaluate {
77            self.run_re_evaluate_pass(
78                token,
79                &mut to_process,
80                &mut processed,
81                &mut result,
82                canceled_paths.as_mut().map(|v| &mut **v),
83            )?;
84        }
85
86        if include_subforms {
87            self.run_subform_pass(changed_paths, re_evaluate, token, &mut result)?;
88        }
89
90        // Deduplicate by $ref — keep the last entry for each path.
91        // Multiple passes (dependents queue, re-evaluate, subform) may independently emit
92        // the same $ref when cache versions cause overlapping detections. The subform pass
93        // result is most specific and wins because it is appended last.
94        let deduped = {
95            let mut seen: IndexMap<String, usize> = IndexMap::new();
96            for (i, item) in result.iter().enumerate() {
97                if let Some(r) = item.get("$ref").and_then(|v| v.as_str()) {
98                    seen.insert(r.to_string(), i);
99                }
100            }
101            let last_indices: IndexSet<usize> = seen.values().copied().collect();
102            let out: Vec<Value> = result
103                .into_iter()
104                .enumerate()
105                .filter(|(i, _)| last_indices.contains(i))
106                .map(|(_, item)| item)
107                .collect();
108            out
109        };
110
111        // Refresh main_form_snapshot so the next evaluate() call computes diffs from the
112        // post-dependents state instead of the old pre-dependents snapshot.
113        // Without this, evaluate() re-diffs every field that evaluate_dependents already
114        // processed, double-bumping data_versions and causing spurious cache misses in
115        // evaluate_internal (observed as unexpected ~550ms "cache hit" full evaluates).
116        // Only update when no subform item is active — subform evaluate_dependents calls
117        // must not overwrite the parent's snapshot.
118        if self.eval_cache.active_item_index.is_none() {
119            let current_snapshot = self.eval_data.snapshot_data_clone();
120            self.eval_cache.main_form_snapshot = Some(current_snapshot);
121        }
122
123        Ok(Value::Array(deduped))
124    }
125
126    /// Full re-evaluation pass: runs `evaluate_internal`, then applies read-only fixes and
127    /// recursive hide effects, feeding any newly-generated changes back into the dependents queue.
128    fn run_re_evaluate_pass(
129        &mut self,
130        token: Option<&CancellationToken>,
131        to_process: &mut Vec<(String, bool)>,
132        processed: &mut IndexSet<String>,
133        result: &mut Vec<Value>,
134        mut canceled_paths: Option<&mut Vec<String>>,
135    ) -> Result<(), String> {
136        // --- Schema Default Value Pass (Before Eval) ---
137        self.run_schema_default_value_pass(
138            token,
139            to_process,
140            processed,
141            result,
142            canceled_paths.as_mut().map(|v| &mut **v),
143        )?;
144
145        // Resolve the correct data_versions tracker before snapshotting.
146        // When active_item_index is Some(idx), evaluate_internal bumps
147        // subform_caches[idx].data_versions — NOT the main data_versions.
148        // Using the main tracker for both snapshot and post-eval lookup would make
149        // old_ver == new_ver always, so no changed values would ever be emitted.
150        let pre_eval_versions = if let Some(idx) = self.eval_cache.active_item_index {
151            self.eval_cache
152                .subform_caches
153                .get(&idx)
154                .map(|c| c.data_versions.clone())
155                .unwrap_or_else(|| self.eval_cache.data_versions.clone())
156        } else {
157            self.eval_cache.data_versions.clone()
158        };
159
160        self.evaluate_internal(None, token)?;
161
162        // --- Schema Default Value Pass (After Eval) ---
163        self.run_schema_default_value_pass(
164            token,
165            to_process,
166            processed,
167            result,
168            canceled_paths.as_mut().map(|v| &mut **v),
169        )?;
170
171        // Emit result entries for every sorted-evaluation whose version uniquely bumped.
172        // Again resolve to the per-item tracker so the comparison uses the same source
173        // that evaluate_internal wrote into.
174        let active_idx = self.eval_cache.active_item_index;
175        for eval_key in self.sorted_evaluations.iter().flatten() {
176            if eval_key.contains("/$params/") || eval_key.contains("/$") {
177                continue;
178            }
179
180            let schema_ptr = path_utils::normalize_to_json_pointer(eval_key);
181            let data_path = schema_ptr
182                .replace("/properties/", "/")
183                .trim_start_matches('#')
184                .trim_start_matches('/')
185                .to_string();
186
187            let version_path = format!("/{}", data_path);
188            let old_ver = pre_eval_versions.get(&version_path);
189            let new_ver = if let Some(idx) = active_idx {
190                self.eval_cache
191                    .subform_caches
192                    .get(&idx)
193                    .map(|c| c.data_versions.get(&version_path))
194                    .unwrap_or_else(|| self.eval_cache.data_versions.get(&version_path))
195            } else {
196                self.eval_cache.data_versions.get(&version_path)
197            };
198
199            if new_ver > old_ver {
200                if let Some(new_val) = self.evaluated_schema.pointer(&schema_ptr) {
201                    let dot_path = data_path.trim_end_matches("/value").replace('/', ".");
202                    let mut obj = serde_json::Map::new();
203                    obj.insert("$ref".to_string(), Value::String(dot_path));
204                    obj.insert("value".to_string(), new_val.clone());
205                    result.push(Value::Object(obj));
206                }
207            }
208        }
209
210        // Re-acquire lock for post-eval passes
211        let _lock = self.eval_lock.lock().unwrap();
212
213        // --- Read-Only Pass ---
214        let mut readonly_changes = Vec::new();
215        let mut readonly_values = Vec::new();
216        for path in self.conditional_readonly_fields.iter() {
217            let normalized = path_utils::normalize_to_json_pointer(path);
218            if let Some(schema_el) = self.evaluated_schema.pointer(&normalized) {
219                self.check_readonly_for_dependents(
220                    schema_el,
221                    path,
222                    &mut readonly_changes,
223                    &mut readonly_values,
224                );
225            }
226        }
227        for (path, schema_value) in readonly_changes {
228            let data_path = path_utils::normalize_to_json_pointer(&path)
229                .replace("/properties/", "/")
230                .trim_start_matches('#')
231                .to_string();
232            self.eval_data.set(&data_path, schema_value.clone());
233            self.eval_cache.bump_data_version(&data_path);
234            to_process.push((path, true));
235        }
236        for (path, schema_value) in readonly_values {
237            let data_path = path_utils::normalize_to_json_pointer(&path)
238                .replace("/properties/", "/")
239                .trim_start_matches('#')
240                .to_string();
241            let mut obj = serde_json::Map::new();
242            obj.insert(
243                "$ref".to_string(),
244                Value::String(path_utils::pointer_to_dot_notation(&data_path)),
245            );
246            obj.insert("$readonly".to_string(), Value::Bool(true));
247            obj.insert("value".to_string(), schema_value);
248            result.push(Value::Object(obj));
249        }
250
251        // When readonly fields were updated during a subform item evaluation, `$params`
252        // tables that aggregate rider data (e.g. WOP_RIDERS) may still hold stale cached
253        // results — their static external_deps do not include individual rider paths like
254        // /riders/loading_benefit/first_prem, so a version bump on first_prem alone is
255        // not enough to bust their T1 cache entry. Re-running evaluate_internal (after
256        // invalidating their T1 entries) forces a fresh recomputation with the updated data.
257        let had_readonly_changes = !to_process.is_empty();
258        if had_readonly_changes {
259            if let Some(active_idx) = self.eval_cache.active_item_index {
260                let params_table_keys: Vec<String> = self
261                    .table_metadata
262                    .keys()
263                    .filter(|k| k.starts_with("#/$params"))
264                    .cloned()
265                    .collect();
266                if !params_table_keys.is_empty() {
267                    self.eval_cache
268                        .invalidate_params_tables_for_item(active_idx, &params_table_keys);
269                }
270                drop(_lock);
271                self.evaluate_internal(None, token)?;
272                // After re-evaluation, we need to explicitly re-acquire the lock if we continue processing
273                // However, since `to_process` loop runs immediately after without needing the exact lock structure here,
274                // we can let the loop acquire what it needs.
275            }
276        }
277
278        if !to_process.is_empty() {
279            Self::process_dependents_queue(
280                &self.engine,
281                &self.evaluations,
282                &mut self.eval_data,
283                &mut self.eval_cache,
284                &self.dependents_evaluations,
285                &self.evaluated_schema,
286                to_process,
287                processed,
288                result,
289                token,
290                canceled_paths.as_mut().map(|v| &mut **v),
291            )?;
292        }
293
294        // --- Recursive Hide Pass ---
295        let mut hidden_fields = Vec::new();
296        for path in self.conditional_hidden_fields.iter() {
297            let normalized = path_utils::normalize_to_json_pointer(path);
298            if let Some(schema_el) = self.evaluated_schema.pointer(&normalized) {
299                self.check_hidden_field(schema_el, path, &mut hidden_fields);
300            }
301        }
302        if !hidden_fields.is_empty() {
303            Self::recursive_hide_effect(
304                &self.engine,
305                &self.evaluations,
306                &self.reffed_by,
307                &mut self.eval_data,
308                &mut self.eval_cache,
309                hidden_fields,
310                to_process,
311                result,
312            );
313        }
314        if !to_process.is_empty() {
315            Self::process_dependents_queue(
316                &self.engine,
317                &self.evaluations,
318                &mut self.eval_data,
319                &mut self.eval_cache,
320                &self.dependents_evaluations,
321                &self.evaluated_schema,
322                to_process,
323                processed,
324                result,
325                token,
326                canceled_paths.as_mut().map(|v| &mut **v),
327            )?;
328        }
329
330        Ok(())
331    }
332
333    /// Internal method to run the schema default value pass.
334    /// Filters for only primitive schema values (not $evaluation objects).
335    fn run_schema_default_value_pass(
336        &mut self,
337        token: Option<&CancellationToken>,
338        to_process: &mut Vec<(String, bool)>,
339        processed: &mut IndexSet<String>,
340        result: &mut Vec<Value>,
341        mut canceled_paths: Option<&mut Vec<String>>,
342    ) -> Result<(), String> {
343        let mut default_value_changes = Vec::new();
344        let schema_values = self.get_schema_value_array();
345
346        if let Value::Array(values) = schema_values {
347            for item in values {
348                if let Value::Object(map) = item {
349                    if let (Some(Value::String(dot_path)), Some(schema_val)) =
350                        (map.get("path"), map.get("value"))
351                    {
352                        let schema_ptr = path_utils::dot_notation_to_schema_pointer(dot_path);
353                        if let Some(Value::Object(schema_node)) = self
354                            .evaluated_schema
355                            .pointer(schema_ptr.trim_start_matches('#'))
356                        {
357                            if let Some(Value::Object(condition)) = schema_node.get("condition") {
358                                if let Some(hidden_val) = condition.get("hidden") {
359                                    // Skip if hidden is true OR if it's a non-primitive value (formula object)
360                                    if !hidden_val.is_boolean()
361                                        || hidden_val.as_bool() == Some(true)
362                                    {
363                                        continue;
364                                    }
365                                }
366                            }
367                        }
368
369                        let data_path = dot_path.replace('.', "/");
370                        let current_data = self
371                            .eval_data
372                            .data()
373                            .pointer(&format!("/{}", data_path))
374                            .unwrap_or(&Value::Null);
375
376                        let is_empty = match current_data {
377                            Value::Null => true,
378                            Value::String(s) if s.is_empty() => true,
379                            _ => false,
380                        };
381
382                        let is_schema_val_empty = match schema_val {
383                            Value::Null => true,
384                            Value::String(s) if s.is_empty() => true,
385                            Value::Object(map) if map.contains_key("$evaluation") => true,
386                            _ => false,
387                        };
388
389                        if is_empty && !is_schema_val_empty && current_data != schema_val {
390                            default_value_changes.push((
391                                data_path,
392                                schema_val.clone(),
393                                dot_path.clone(),
394                            ));
395                        }
396                    }
397                }
398            }
399        }
400
401        let mut has_changes = false;
402        for (data_path, schema_val, dot_path) in default_value_changes {
403            self.eval_data
404                .set(&format!("/{}", data_path), schema_val.clone());
405            self.eval_cache
406                .bump_data_version(&format!("/{}", data_path));
407
408            let mut change_obj = serde_json::Map::new();
409            change_obj.insert("$ref".to_string(), Value::String(dot_path));
410            change_obj.insert("value".to_string(), schema_val);
411            result.push(Value::Object(change_obj));
412
413            let schema_ptr = format!("#/{}", data_path.replace('/', "/properties/"));
414            to_process.push((schema_ptr, true));
415            has_changes = true;
416        }
417
418        if has_changes {
419            Self::process_dependents_queue(
420                &self.engine,
421                &self.evaluations,
422                &mut self.eval_data,
423                &mut self.eval_cache,
424                &self.dependents_evaluations,
425                &self.evaluated_schema,
426                to_process,
427                processed,
428                result,
429                token,
430                canceled_paths.as_mut().map(|v| &mut **v),
431            )?;
432        }
433
434        Ok(())
435    }
436
437    /// Cascade dependency evaluation into each subform item.
438    ///
439    /// For every registered subform, this method iterates over its array items and runs
440    /// `evaluate_dependents` on the subform using the cache-swap strategy so the subform
441    /// can see global main-form Tier 2 cache entries (avoiding redundant table re-evaluation).
442    ///
443    /// `sub_re_evaluate` is set **only** when the parent's bumped `data_versions` intersect
444    /// with paths the subform actually depends on — preventing expensive full re-evals on
445    /// subform items whose dependencies did not change.
446    fn run_subform_pass(
447        &mut self,
448        changed_paths: &[String],
449        re_evaluate: bool,
450        token: Option<&CancellationToken>,
451        result: &mut Vec<Value>,
452    ) -> Result<(), String> {
453        // Collect subform paths once (avoids holding borrow on self.subforms during mutation)
454        let subform_paths: Vec<String> = self.subforms.keys().cloned().collect();
455
456        for subform_path in subform_paths {
457            let field_key = subform_field_key(&subform_path);
458            // Compute dotted path and prefix strings once per subform, not per item
459            let subform_dot_path =
460                path_utils::pointer_to_dot_notation(&subform_path).replace(".properties.", ".");
461            let field_prefix = format!("{}.", field_key);
462            let subform_ptr = normalize_to_json_pointer(&subform_path);
463
464            // Borrow only the item count first — avoid cloning the full array
465            let item_count =
466                get_value_by_pointer_without_properties(self.eval_data.data(), &subform_ptr)
467                    .and_then(|v| v.as_array())
468                    .map(|a| a.len())
469                    .unwrap_or(0);
470
471            if item_count == 0 {
472                continue;
473            }
474
475            // Evict stale per-item caches for indices that no longer exist in the array.
476            // This prevents memory leaks when riders are removed and the array shrinks.
477            self.eval_cache.prune_subform_caches(item_count);
478
479            // When the parent ran a re_evaluate pass, always pass re_evaluate:true to subforms.
480            // The parent's evaluate_internal may have updated $params or other referenced values
481            // that the subform formulas read, even if none of the subform's own dep paths bumped.
482            let global_sub_re_evaluate = re_evaluate;
483
484            // Snapshot the parent's version trackers once, before iterating any riders.
485            // Using the live `parent_cache.data_versions` inside the loop would let rider N's
486            // evaluation bumps contaminate the merge_from baseline for rider M (M ≠ N),
487            // causing cache misses and wrong re-evaluations on subsequent visits to rider M.
488            let parent_data_versions_snapshot = self.eval_cache.data_versions.clone();
489            let parent_params_versions_snapshot = self.eval_cache.params_versions.clone();
490
491            for idx in 0..item_count {
492                // Map absolute changed paths → subform-internal paths for this item index
493                let prefix_dot = format!("{}.{}.", subform_dot_path, idx);
494                let prefix_bracket = format!("{}[{}].", subform_dot_path, idx);
495                let prefix_field_bracket = format!("{}[{}].", field_key, idx);
496
497                let item_changed_paths: Vec<String> = changed_paths
498                    .iter()
499                    .filter_map(|p| {
500                        if p.starts_with(&prefix_bracket) {
501                            Some(p.replacen(&prefix_bracket, &field_prefix, 1))
502                        } else if p.starts_with(&prefix_dot) {
503                            Some(p.replacen(&prefix_dot, &field_prefix, 1))
504                        } else if p.starts_with(&prefix_field_bracket) {
505                            Some(p.replacen(&prefix_field_bracket, &field_prefix, 1))
506                        } else {
507                            None
508                        }
509                    })
510                    .collect();
511
512                let sub_re_evaluate = global_sub_re_evaluate || !item_changed_paths.is_empty();
513
514                // Skip entirely if there's nothing to do for this item
515                if !sub_re_evaluate && item_changed_paths.is_empty() {
516                    continue;
517                }
518
519                // Build minimal merged data: clone only item at idx, share $params shallowly.
520                // This avoids cloning the full 5MB parent payload for every item.
521                let item_val =
522                    get_value_by_pointer_without_properties(self.eval_data.data(), &subform_ptr)
523                        .and_then(|v| v.as_array())
524                        .and_then(|a| a.get(idx))
525                        .cloned()
526                        .unwrap_or(Value::Null);
527
528                // Build a minimal parent object with only the fields the subform needs:
529                // the item under field_key, plus all non-array top-level parent fields
530                // ($params markers, scalars). Large arrays are already stripped to static_arrays.
531                let merged_data = {
532                    let parent = self.eval_data.data();
533                    let mut map = serde_json::Map::new();
534                    if let Value::Object(parent_map) = parent {
535                        for (k, v) in parent_map {
536                            if k == &field_key {
537                                // Will be overridden with the single item below
538                                continue;
539                            }
540                            // Include scalars, objects ($params markers, etc.) but skip
541                            // other large array fields that aren't this subform
542                            if !v.is_array() {
543                                map.insert(k.clone(), v.clone());
544                            }
545                        }
546                    }
547                    map.insert(field_key.clone(), item_val.clone());
548                    Value::Object(map)
549                };
550
551                let Some(subform) = self.subforms.get_mut(&subform_path) else {
552                    continue;
553                };
554
555                // Prepare cache state for this item
556                self.eval_cache.ensure_active_item_cache(idx);
557                let old_item_val = self
558                    .eval_cache
559                    .subform_caches
560                    .get(&idx)
561                    .map(|c| c.item_snapshot.clone())
562                    .unwrap_or(Value::Null);
563
564                subform.eval_data.replace_data_and_context(
565                    merged_data,
566                    self.eval_data
567                        .data()
568                        .get("$context")
569                        .cloned()
570                        .unwrap_or(Value::Null),
571                );
572                let new_item_val = subform
573                    .eval_data
574                    .data()
575                    .get(&field_key)
576                    .cloned()
577                    .unwrap_or(Value::Null);
578
579                // Cache-swap: lend parent cache to subform
580                let mut parent_cache = std::mem::take(&mut self.eval_cache);
581                parent_cache.ensure_active_item_cache(idx);
582                if let Some(c) = parent_cache.subform_caches.get_mut(&idx) {
583                    // Merge all data versions from the parent snapshot. We must include non-$params
584                    // paths so that parent field updates (like wop_basic_benefit changing) correctly
585                    // invalidate subform per-item cache entries that depend on them.
586                    c.data_versions.merge_from(&parent_data_versions_snapshot);
587                    // Always reflect the latest $params (schema-level, index-independent).
588                    c.data_versions
589                        .merge_from_params(&parent_params_versions_snapshot);
590                    crate::jsoneval::eval_cache::diff_and_update_versions(
591                        &mut c.data_versions,
592                        &format!("/{}", field_key),
593                        &old_item_val,
594                        &new_item_val,
595                    );
596                    c.item_snapshot = new_item_val;
597                }
598                parent_cache.set_active_item(idx);
599                std::mem::swap(&mut subform.eval_cache, &mut parent_cache);
600
601                let subform_result = subform.evaluate_dependents(
602                    &item_changed_paths,
603                    None,
604                    None,
605                    sub_re_evaluate,
606                    token,
607                    None,
608                    false,
609                );
610
611                // Restore parent cache
612                std::mem::swap(&mut subform.eval_cache, &mut parent_cache);
613                parent_cache.clear_active_item();
614
615                // Propagate the updated item_snapshot from the parent's T1 cache into the
616                // subform's own eval_cache. Without this, subsequent evaluate_subform() calls
617                // for this idx read the OLD snapshot (pre-run_subform_pass) and see a diff
618                // against the new data → item_paths_bumped = true → spurious table invalidation.
619                if let Some(parent_item_cache) = self.eval_cache.subform_caches.get(&idx) {
620                    let snapshot = parent_item_cache.item_snapshot.clone();
621                    subform
622                        .eval_cache
623                        .ensure_active_item_cache(idx);
624                    if let Some(sub_cache) = subform.eval_cache.subform_caches.get_mut(&idx) {
625                        sub_cache.item_snapshot = snapshot;
626                    }
627                }
628
629                self.eval_cache = parent_cache;
630
631                if let Ok(Value::Array(changes)) = subform_result {
632                    for change in changes {
633                        if let Some(obj) = change.as_object() {
634                            if let Some(Value::String(ref_path)) = obj.get("$ref") {
635                                // Remap the $ref path to include the parent path + item index
636                                let new_ref = if ref_path.starts_with(&field_prefix) {
637                                    format!(
638                                        "{}.{}.{}",
639                                        subform_dot_path,
640                                        idx,
641                                        &ref_path[field_prefix.len()..]
642                                    )
643                                } else {
644                                    format!("{}.{}.{}", subform_dot_path, idx, ref_path)
645                                };
646
647                                // Write the computed value back to parent eval_data so subsequent
648                                // evaluate_subform calls see an up-to-date old_item_snapshot.
649                                // Without this, the diff in with_item_cache_swap sees stale parent
650                                // data vs the new call's apply_changes values → spurious item bumps
651                                // → invalidate_params_tables_for_item fires → eval_generation bumps.
652                                if let Some(val) = obj.get("value") {
653                                    let data_ptr = format!(
654                                        "/{}",
655                                        new_ref.replace('.', "/")
656                                    );
657                                    self.eval_data.set(&data_ptr, val.clone());
658                                } else if obj.get("clear").and_then(Value::as_bool) == Some(true) {
659                                    let data_ptr = format!(
660                                        "/{}",
661                                        new_ref.replace('.', "/")
662                                    );
663                                    self.eval_data.set(&data_ptr, Value::Null);
664                                }
665
666                                let mut new_obj = obj.clone();
667                                new_obj.insert("$ref".to_string(), Value::String(new_ref));
668                                result.push(Value::Object(new_obj));
669                            } else {
670                                // No $ref rewrite needed — push as-is without cloning the map
671                                result.push(change);
672                            }
673                        }
674                    }
675                }
676            }
677        }
678        Ok(())
679    }
680
681    /// Helper to evaluate a dependent value - uses pre-compiled eval keys for fast lookup
682    pub(crate) fn evaluate_dependent_value_static(
683        engine: &RLogic,
684        evaluations: &IndexMap<String, LogicId>,
685        eval_data: &EvalData,
686        value: &Value,
687        changed_field_value: &Value,
688        changed_field_ref_value: &Value,
689    ) -> Result<Value, String> {
690        match value {
691            // If it's a String, check if it's an eval key reference
692            Value::String(eval_key) => {
693                if let Some(logic_id) = evaluations.get(eval_key) {
694                    // It's a pre-compiled evaluation - run it with scoped context
695                    // Create internal context with $value and $refValue
696                    let mut internal_context = serde_json::Map::new();
697                    internal_context.insert("$value".to_string(), changed_field_value.clone());
698                    internal_context.insert("$refValue".to_string(), changed_field_ref_value.clone());
699                    let context_value = Value::Object(internal_context);
700
701                    let result = engine.run_with_context(logic_id, eval_data.data(), &context_value)
702                        .map_err(|e| format!("Failed to evaluate dependent logic '{}': {}", eval_key, e))?;
703                    Ok(result)
704                } else {
705                    // It's a regular string value
706                    Ok(value.clone())
707                }
708            }
709            // For backwards compatibility: compile $evaluation on-the-fly
710            // This shouldn't happen with properly parsed schemas
711            Value::Object(map) if map.contains_key("$evaluation") => {
712                Err("Dependent evaluation contains unparsed $evaluation - schema was not properly parsed".to_string())
713            }
714            // Primitive value - return as-is
715            _ => Ok(value.clone()),
716        }
717    }
718
719    /// Check if a single field is readonly and populate vectors for both changes and all values
720    pub(crate) fn check_readonly_for_dependents(
721        &self,
722        schema_element: &Value,
723        path: &str,
724        changes: &mut Vec<(String, Value)>,
725        all_values: &mut Vec<(String, Value)>,
726    ) {
727        match schema_element {
728            Value::Object(map) => {
729                // Check if field is disabled (ReadOnly)
730                let mut is_disabled = false;
731                if let Some(Value::Object(condition)) = map.get("condition") {
732                    if let Some(Value::Bool(d)) = condition.get("disabled") {
733                        is_disabled = *d;
734                    }
735                }
736
737                // Check skipReadOnlyValue config
738                let mut skip_readonly = false;
739                if let Some(Value::Object(config)) = map.get("config") {
740                    if let Some(Value::Object(all)) = config.get("all") {
741                        if let Some(Value::Bool(skip)) = all.get("skipReadOnlyValue") {
742                            skip_readonly = *skip;
743                        }
744                    }
745                }
746
747                if is_disabled && !skip_readonly {
748                    if let Some(schema_value) = map.get("value") {
749                        let data_path = path_utils::normalize_to_json_pointer(path)
750                            .replace("/properties/", "/")
751                            .trim_start_matches('#')
752                            .to_string();
753
754                        let current_data = self
755                            .eval_data
756                            .data()
757                            .pointer(&data_path)
758                            .unwrap_or(&Value::Null);
759
760                        // Add to all_values (include in dependents result regardless of change)
761                        all_values.push((path.to_string(), schema_value.clone()));
762
763                        // Only add to changes if value doesn't match
764                        if current_data != schema_value {
765                            changes.push((path.to_string(), schema_value.clone()));
766                        }
767                    }
768                }
769            }
770            _ => {}
771        }
772    }
773
774    /// Recursively collect read-only fields that need updates (Legacy/Full-Scan)
775    #[allow(dead_code)]
776    pub(crate) fn collect_readonly_fixes(
777        &self,
778        schema_element: &Value,
779        path: &str,
780        changes: &mut Vec<(String, Value)>,
781    ) {
782        match schema_element {
783            Value::Object(map) => {
784                // Check if field is disabled (ReadOnly)
785                let mut is_disabled = false;
786                if let Some(Value::Object(condition)) = map.get("condition") {
787                    if let Some(Value::Bool(d)) = condition.get("disabled") {
788                        is_disabled = *d;
789                    }
790                }
791
792                // Check skipReadOnlyValue config
793                let mut skip_readonly = false;
794                if let Some(Value::Object(config)) = map.get("config") {
795                    if let Some(Value::Object(all)) = config.get("all") {
796                        if let Some(Value::Bool(skip)) = all.get("skipReadOnlyValue") {
797                            skip_readonly = *skip;
798                        }
799                    }
800                }
801
802                if is_disabled && !skip_readonly {
803                    // Check if it's a value field (has "value" property or implicit via path?)
804                    // In JS: "const readOnlyValues = this.getSchemaValues();"
805                    // We only care if data != schema value
806                    if let Some(schema_value) = map.get("value") {
807                        let data_path = path_utils::normalize_to_json_pointer(path)
808                            .replace("/properties/", "/")
809                            .trim_start_matches('#')
810                            .to_string();
811
812                        let current_data = self
813                            .eval_data
814                            .data()
815                            .pointer(&data_path)
816                            .unwrap_or(&Value::Null);
817
818                        if current_data != schema_value {
819                            changes.push((path.to_string(), schema_value.clone()));
820                        }
821                    }
822                }
823
824                // Recurse into properties
825                if let Some(Value::Object(props)) = map.get("properties") {
826                    for (key, val) in props {
827                        let next_path = if path == "#" {
828                            format!("#/properties/{}", key)
829                        } else {
830                            format!("{}/properties/{}", path, key)
831                        };
832                        self.collect_readonly_fixes(val, &next_path, changes);
833                    }
834                }
835            }
836            _ => {}
837        }
838    }
839
840    /// Check if a single field is hidden and needs clearing (Optimized non-recursive)
841    pub(crate) fn check_hidden_field(
842        &self,
843        schema_element: &Value,
844        path: &str,
845        hidden_fields: &mut Vec<String>,
846    ) {
847        match schema_element {
848            Value::Object(map) => {
849                // Check if field is hidden
850                let mut is_hidden = false;
851                if let Some(Value::Object(condition)) = map.get("condition") {
852                    if let Some(Value::Bool(h)) = condition.get("hidden") {
853                        is_hidden = *h;
854                    }
855                }
856
857                // Check keepHiddenValue config
858                let mut keep_hidden = false;
859                if let Some(Value::Object(config)) = map.get("config") {
860                    if let Some(Value::Object(all)) = config.get("all") {
861                        if let Some(Value::Bool(keep)) = all.get("keepHiddenValue") {
862                            keep_hidden = *keep;
863                        }
864                    }
865                }
866
867                if is_hidden && !keep_hidden {
868                    let data_path = path_utils::normalize_to_json_pointer(path)
869                        .replace("/properties/", "/")
870                        .trim_start_matches('#')
871                        .to_string();
872
873                    let current_data = self
874                        .eval_data
875                        .data()
876                        .pointer(&data_path)
877                        .unwrap_or(&Value::Null);
878
879                    // If hidden and has non-empty value, add to list
880                    if current_data != &Value::Null && current_data != "" {
881                        hidden_fields.push(path.to_string());
882                    }
883                }
884            }
885            _ => {}
886        }
887    }
888
889    /// Recursively collect hidden fields that have values (candidates for clearing) (Legacy/Full-Scan)
890    #[allow(dead_code)]
891    pub(crate) fn collect_hidden_fields(
892        &self,
893        schema_element: &Value,
894        path: &str,
895        hidden_fields: &mut Vec<String>,
896    ) {
897        match schema_element {
898            Value::Object(map) => {
899                // Check if field is hidden
900                let mut is_hidden = false;
901                if let Some(Value::Object(condition)) = map.get("condition") {
902                    if let Some(Value::Bool(h)) = condition.get("hidden") {
903                        is_hidden = *h;
904                    }
905                }
906
907                // Check keepHiddenValue config
908                let mut keep_hidden = false;
909                if let Some(Value::Object(config)) = map.get("config") {
910                    if let Some(Value::Object(all)) = config.get("all") {
911                        if let Some(Value::Bool(keep)) = all.get("keepHiddenValue") {
912                            keep_hidden = *keep;
913                        }
914                    }
915                }
916
917                if is_hidden && !keep_hidden {
918                    let data_path = path_utils::normalize_to_json_pointer(path)
919                        .replace("/properties/", "/")
920                        .trim_start_matches('#')
921                        .to_string();
922
923                    let current_data = self
924                        .eval_data
925                        .data()
926                        .pointer(&data_path)
927                        .unwrap_or(&Value::Null);
928
929                    // If hidden and has non-empty value, add to list
930                    if current_data != &Value::Null && current_data != "" {
931                        hidden_fields.push(path.to_string());
932                    }
933                }
934
935                // Recurse into children
936                for (key, val) in map {
937                    if key == "properties" {
938                        if let Value::Object(props) = val {
939                            for (p_key, p_val) in props {
940                                let next_path = if path == "#" {
941                                    format!("#/properties/{}", p_key)
942                                } else {
943                                    format!("{}/properties/{}", path, p_key)
944                                };
945                                self.collect_hidden_fields(p_val, &next_path, hidden_fields);
946                            }
947                        }
948                    } else if let Value::Object(_) = val {
949                        // Skip known metadata keys and explicitly handled keys
950                        if key == "condition"
951                            || key == "config"
952                            || key == "rules"
953                            || key == "dependents"
954                            || key == "hideLayout"
955                            || key == "$layout"
956                            || key == "$params"
957                            || key == "definitions"
958                            || key == "$defs"
959                            || key.starts_with('$')
960                        {
961                            continue;
962                        }
963
964                        let next_path = if path == "#" {
965                            format!("#/{}", key)
966                        } else {
967                            format!("{}/{}", path, key)
968                        };
969                        self.collect_hidden_fields(val, &next_path, hidden_fields);
970                    }
971                }
972            }
973            _ => {}
974        }
975    }
976
977    /// Perform recursive hiding effect using reffed_by graph.
978    /// Collects every data path that gets nulled into `invalidated_paths`.
979    pub(crate) fn recursive_hide_effect(
980        engine: &RLogic,
981        evaluations: &IndexMap<String, LogicId>,
982        reffed_by: &IndexMap<String, Vec<String>>,
983        eval_data: &mut EvalData,
984        eval_cache: &mut crate::jsoneval::eval_cache::EvalCache,
985        mut hidden_fields: Vec<String>,
986        queue: &mut Vec<(String, bool)>,
987        result: &mut Vec<Value>,
988    ) {
989        while let Some(hf) = hidden_fields.pop() {
990            let data_path = path_utils::normalize_to_json_pointer(&hf)
991                .replace("/properties/", "/")
992                .trim_start_matches('#')
993                .to_string();
994
995            // clear data
996            eval_data.set(&data_path, Value::Null);
997            eval_cache.bump_data_version(&data_path);
998
999            // Create dependent object for result
1000            let mut change_obj = serde_json::Map::new();
1001            change_obj.insert(
1002                "$ref".to_string(),
1003                Value::String(path_utils::pointer_to_dot_notation(&data_path)),
1004            );
1005            change_obj.insert("$hidden".to_string(), Value::Bool(true));
1006            change_obj.insert("clear".to_string(), Value::Bool(true));
1007            result.push(Value::Object(change_obj));
1008
1009            // Add to queue for standard dependent processing
1010            queue.push((hf.clone(), true));
1011
1012            // Check reffed_by to find other fields that might become hidden
1013            if let Some(referencing_fields) = reffed_by.get(&data_path) {
1014                for rb in referencing_fields {
1015                    // Evaluate condition.hidden for rb
1016                    // We need a way to run specific evaluation?
1017                    // We can check if rb has a hidden evaluation in self.evaluations
1018                    let hidden_eval_key = format!("{}/condition/hidden", rb);
1019
1020                    if let Some(logic_id) = evaluations.get(&hidden_eval_key) {
1021                        // Run evaluation
1022                        // Context: $value = current field (rb) value? No, $value usually refers to changed field in deps.
1023                        // But here we are just re-evaluating the rule.
1024                        // In JS logic: "const result = hiddenFn(runnerCtx);"
1025                        // runnerCtx has the updated data (we just set hf to null).
1026
1027                        let rb_data_path = path_utils::normalize_to_json_pointer(rb)
1028                            .replace("/properties/", "/")
1029                            .trim_start_matches('#')
1030                            .to_string();
1031                        let rb_value = eval_data
1032                            .data()
1033                            .pointer(&rb_data_path)
1034                            .cloned()
1035                            .unwrap_or(Value::Null);
1036
1037                        // We can use engine.run w/ eval_data
1038                        if let Ok(Value::Bool(is_hidden)) = engine.run(logic_id, eval_data.data()) {
1039                            if is_hidden {
1040                                // Check if rb is not already in hidden_fields and has value
1041                                // rb is &String, hidden_fields is Vec<String>
1042                                if !hidden_fields.contains(rb) {
1043                                    let has_value = rb_value != Value::Null && rb_value != "";
1044                                    if has_value {
1045                                        hidden_fields.push(rb.clone());
1046                                    }
1047                                }
1048                            }
1049                        }
1050                    }
1051                }
1052            }
1053        }
1054    }
1055
1056    /// Process the dependents queue.
1057    /// Collects every data path written into `eval_data` into `invalidated_paths`.
1058    pub(crate) fn process_dependents_queue(
1059        engine: &RLogic,
1060        evaluations: &IndexMap<String, LogicId>,
1061        eval_data: &mut EvalData,
1062        eval_cache: &mut crate::jsoneval::eval_cache::EvalCache,
1063        dependents_evaluations: &IndexMap<String, Vec<DependentItem>>,
1064        evaluated_schema: &Value,
1065        queue: &mut Vec<(String, bool)>,
1066        processed: &mut IndexSet<String>,
1067        result: &mut Vec<Value>,
1068        token: Option<&CancellationToken>,
1069        canceled_paths: Option<&mut Vec<String>>,
1070    ) -> Result<(), String> {
1071        while let Some((current_path, is_transitive)) = queue.pop() {
1072            if let Some(t) = token {
1073                if t.is_cancelled() {
1074                    // Accumulate canceled paths if buffer provided
1075                    if let Some(cp) = canceled_paths {
1076                        cp.push(current_path.clone());
1077                        // Also push remaining items in queue?
1078                        // The user request says "accumulate canceled path if provided", usually implies what was actively cancelled
1079                        // or what was pending. Since we pop one by one, we can just dump the queue back or just push pending.
1080                        // But since we just popped `current_path`, it is the one being cancelled on.
1081                        // Let's also drain the queue.
1082                        for (path, _) in queue.iter() {
1083                            cp.push(path.clone());
1084                        }
1085                    }
1086                    return Err("Cancelled".to_string());
1087                }
1088            }
1089            if processed.contains(&current_path) {
1090                continue;
1091            }
1092            processed.insert(current_path.clone());
1093
1094            // Get the value of the changed field for $value context
1095            let current_data_path = path_utils::normalize_to_json_pointer(&current_path)
1096                .replace("/properties/", "/")
1097                .trim_start_matches('#')
1098                .to_string();
1099            let mut current_value = eval_data
1100                .data()
1101                .pointer(&current_data_path)
1102                .cloned()
1103                .unwrap_or(Value::Null);
1104
1105            // Find dependents for this path
1106            if let Some(dependent_items) = dependents_evaluations.get(&current_path) {
1107                for dep_item in dependent_items {
1108                    let ref_path = &dep_item.ref_path;
1109                    let pointer_path = path_utils::normalize_to_json_pointer(ref_path);
1110                    // Data paths don't include /properties/, strip it for data access
1111                    let data_path = pointer_path.replace("/properties/", "/");
1112
1113                    let current_ref_value = eval_data
1114                        .data()
1115                        .pointer(&data_path)
1116                        .cloned()
1117                        .unwrap_or(Value::Null);
1118
1119                    // Get field and parent field from schema
1120                    let field = evaluated_schema.pointer(&pointer_path).cloned();
1121
1122                    // Get parent field - skip /properties/ to get actual parent object
1123                    let parent_path = if let Some(last_slash) = pointer_path.rfind("/properties") {
1124                        &pointer_path[..last_slash]
1125                    } else {
1126                        "/"
1127                    };
1128                    let mut parent_field = if parent_path.is_empty() || parent_path == "/" {
1129                        evaluated_schema.clone()
1130                    } else {
1131                        evaluated_schema
1132                            .pointer(parent_path)
1133                            .cloned()
1134                            .unwrap_or_else(|| Value::Object(serde_json::Map::new()))
1135                    };
1136
1137                    // omit properties to minimize size of parent field
1138                    if let Value::Object(ref mut map) = parent_field {
1139                        map.remove("properties");
1140                        map.remove("$layout");
1141                    }
1142
1143                    let mut change_obj = serde_json::Map::new();
1144                    change_obj.insert(
1145                        "$ref".to_string(),
1146                        Value::String(path_utils::pointer_to_dot_notation(&data_path)),
1147                    );
1148                    if let Some(f) = field {
1149                        change_obj.insert("$field".to_string(), f);
1150                    }
1151                    change_obj.insert("$parentField".to_string(), parent_field);
1152                    change_obj.insert("transitive".to_string(), Value::Bool(is_transitive));
1153
1154                    let mut add_transitive = false;
1155                    let mut add_deps = false;
1156                    // Process clear
1157                    if let Some(clear_val) = &dep_item.clear {
1158                        let should_clear = Self::evaluate_dependent_value_static(
1159                            engine,
1160                            evaluations,
1161                            eval_data,
1162                            clear_val,
1163                            &current_value,
1164                            &current_ref_value,
1165                        )?;
1166                        let clear_bool = match should_clear {
1167                            Value::Bool(b) => b,
1168                            _ => false,
1169                        };
1170
1171                        if clear_bool {
1172                            if data_path == current_data_path {
1173                                current_value = Value::Null;
1174                            }
1175                            eval_data.set(&data_path, Value::Null);
1176                            eval_cache.bump_data_version(&data_path);
1177                            change_obj.insert("clear".to_string(), Value::Bool(true));
1178                            add_transitive = true;
1179                            add_deps = true;
1180                        }
1181                    }
1182
1183                    // Process value
1184                    if let Some(value_val) = &dep_item.value {
1185                        let computed_value = Self::evaluate_dependent_value_static(
1186                            engine,
1187                            evaluations,
1188                            eval_data,
1189                            value_val,
1190                            &current_value,
1191                            &current_ref_value,
1192                        )?;
1193                        let cleaned_val = clean_float_noise_scalar(computed_value);
1194
1195                        if cleaned_val != current_ref_value && cleaned_val != Value::Null {
1196                            if data_path == current_data_path {
1197                                current_value = cleaned_val.clone();
1198                            }
1199                            eval_data.set(&data_path, cleaned_val.clone());
1200                            eval_cache.bump_data_version(&data_path);
1201                            change_obj.insert("value".to_string(), cleaned_val);
1202                            add_transitive = true;
1203                            add_deps = true;
1204                        }
1205                    }
1206
1207                    // add only when has clear / value
1208                    if add_deps {
1209                        result.push(Value::Object(change_obj));
1210                    }
1211
1212                    // Add this dependent to queue for transitive processing
1213                    if add_transitive {
1214                        queue.push((ref_path.clone(), true));
1215                    }
1216                }
1217            }
1218        }
1219        Ok(())
1220    }
1221}
1222
1223/// Extract the field key from a subform path.
1224///
1225/// Examples:
1226/// - `#/riders`                               → `riders`
1227/// - `#/properties/form/properties/riders`    → `riders`
1228/// - `#/items`                                → `items`
1229fn subform_field_key(subform_path: &str) -> String {
1230    // Strip leading `#/`
1231    let stripped = subform_path.trim_start_matches('#').trim_start_matches('/');
1232
1233    // The last non-"properties" segment is the field key
1234    stripped
1235        .split('/')
1236        .filter(|seg| !seg.is_empty() && *seg != "properties")
1237        .last()
1238        .unwrap_or(stripped)
1239        .to_string()
1240}