Skip to main content

sz_configtool_lib/
thresholds.rs

1use crate::error::{Result, SzConfigError};
2use crate::helpers;
3use serde_json::{Value, json};
4
5// ============================================================================
6// Parameter Structs
7// ============================================================================
8
9/// Parameters for adding a comparison threshold
10#[derive(Debug, Clone, Default)]
11pub struct AddComparisonThresholdParams<'a> {
12    pub cfunc_code: Option<&'a str>,
13    pub ftype_code: Option<&'a str>,
14    pub cfunc_rtnval: Option<&'a str>,
15    pub exec_order: Option<i64>,
16    pub same_score: Option<i64>,
17    pub close_score: Option<i64>,
18    pub likely_score: Option<i64>,
19    pub plausible_score: Option<i64>,
20    pub un_likely_score: Option<i64>,
21}
22
23impl<'a> AddComparisonThresholdParams<'a> {
24    pub fn new(cfunc_code: &'a str, ftype_code: &'a str, cfunc_rtnval: &'a str) -> Self {
25        Self {
26            cfunc_code: Some(cfunc_code),
27            ftype_code: Some(ftype_code),
28            cfunc_rtnval: Some(cfunc_rtnval),
29            ..Default::default()
30        }
31    }
32}
33
34impl<'a> TryFrom<&'a Value> for AddComparisonThresholdParams<'a> {
35    type Error = SzConfigError;
36
37    fn try_from(json: &'a Value) -> Result<Self> {
38        Ok(Self {
39            cfunc_code: json.get("cfuncCode").and_then(|v| v.as_str()),
40            ftype_code: json.get("ftypeCode").and_then(|v| v.as_str()),
41            cfunc_rtnval: json.get("cfuncRtnval").and_then(|v| v.as_str()),
42            exec_order: json.get("execOrder").and_then(|v| v.as_i64()),
43            same_score: json.get("sameScore").and_then(|v| v.as_i64()),
44            close_score: json.get("closeScore").and_then(|v| v.as_i64()),
45            likely_score: json.get("likelyScore").and_then(|v| v.as_i64()),
46            plausible_score: json.get("plausibleScore").and_then(|v| v.as_i64()),
47            un_likely_score: json.get("unlikelyScore").and_then(|v| v.as_i64()),
48        })
49    }
50}
51
52/// Parameters for adding a generic threshold
53#[derive(Debug, Clone)]
54pub struct AddGenericThresholdParams<'a> {
55    pub plan: Option<&'a str>,
56    pub behavior: Option<&'a str>,
57    pub scoring_cap: Option<i64>,
58    pub candidate_cap: Option<i64>,
59    pub send_to_redo: Option<&'a str>,
60    pub feature: Option<&'a str>,
61}
62
63impl<'a> AddGenericThresholdParams<'a> {
64    pub fn new(
65        plan: &'a str,
66        behavior: &'a str,
67        scoring_cap: i64,
68        candidate_cap: i64,
69        send_to_redo: &'a str,
70    ) -> Self {
71        Self {
72            plan: Some(plan),
73            behavior: Some(behavior),
74            scoring_cap: Some(scoring_cap),
75            candidate_cap: Some(candidate_cap),
76            send_to_redo: Some(send_to_redo),
77            feature: None,
78        }
79    }
80}
81
82impl<'a> TryFrom<&'a Value> for AddGenericThresholdParams<'a> {
83    type Error = SzConfigError;
84
85    fn try_from(json: &'a Value) -> Result<Self> {
86        Ok(Self {
87            plan: json.get("plan").and_then(|v| v.as_str()),
88            behavior: json.get("behavior").and_then(|v| v.as_str()),
89            scoring_cap: json.get("scoringCap").and_then(|v| v.as_i64()),
90            candidate_cap: json.get("candidateCap").and_then(|v| v.as_i64()),
91            send_to_redo: json.get("sendToRedo").and_then(|v| v.as_str()),
92            feature: json.get("feature").and_then(|v| v.as_str()),
93        })
94    }
95}
96
97/// Parameters for setting (updating) a comparison threshold
98#[derive(Debug, Clone, Default)]
99pub struct SetComparisonThresholdParams<'a> {
100    pub cfunc_code: Option<&'a str>,
101    pub ftype_code: Option<&'a str>,
102    pub cfunc_rtnval: Option<&'a str>,
103    pub exec_order: Option<i64>,
104    pub same_score: Option<i64>,
105    pub close_score: Option<i64>,
106    pub likely_score: Option<i64>,
107    pub plausible_score: Option<i64>,
108    pub un_likely_score: Option<i64>,
109}
110
111impl<'a> TryFrom<&'a Value> for SetComparisonThresholdParams<'a> {
112    type Error = SzConfigError;
113
114    fn try_from(json: &'a Value) -> Result<Self> {
115        Ok(Self {
116            cfunc_code: json.get("cfuncCode").and_then(|v| v.as_str()),
117            ftype_code: json.get("ftypeCode").and_then(|v| v.as_str()),
118            cfunc_rtnval: json.get("cfuncRtnval").and_then(|v| v.as_str()),
119            exec_order: json.get("execOrder").and_then(|v| v.as_i64()),
120            same_score: json.get("sameScore").and_then(|v| v.as_i64()),
121            close_score: json.get("closeScore").and_then(|v| v.as_i64()),
122            likely_score: json.get("likelyScore").and_then(|v| v.as_i64()),
123            plausible_score: json.get("plausibleScore").and_then(|v| v.as_i64()),
124            un_likely_score: json.get("unlikelyScore").and_then(|v| v.as_i64()),
125        })
126    }
127}
128
129/// Parameters for setting (updating) a generic threshold
130#[derive(Debug, Clone)]
131pub struct SetGenericThresholdParams<'a> {
132    pub plan: Option<&'a str>,
133    pub behavior: Option<&'a str>,
134    pub feature: Option<&'a str>,
135    pub candidate_cap: Option<i64>,
136    pub scoring_cap: Option<i64>,
137    pub send_to_redo: Option<&'a str>,
138}
139
140impl<'a> TryFrom<&'a Value> for SetGenericThresholdParams<'a> {
141    type Error = SzConfigError;
142
143    fn try_from(json: &'a Value) -> Result<Self> {
144        Ok(Self {
145            plan: json.get("plan").and_then(|v| v.as_str()),
146            behavior: json.get("behavior").and_then(|v| v.as_str()),
147            feature: json.get("feature").and_then(|v| v.as_str()),
148            candidate_cap: json.get("candidateCap").and_then(|v| v.as_i64()),
149            scoring_cap: json.get("scoringCap").and_then(|v| v.as_i64()),
150            send_to_redo: json.get("sendToRedo").and_then(|v| v.as_str()),
151        })
152    }
153}
154
155/// Parameters for deleting a generic threshold
156#[derive(Debug, Clone, Default)]
157pub struct DeleteGenericThresholdParams<'a> {
158    pub plan: Option<&'a str>,
159    pub behavior: Option<&'a str>,
160    pub feature: Option<&'a str>,
161}
162
163impl<'a> DeleteGenericThresholdParams<'a> {
164    pub fn new(plan: &'a str, behavior: &'a str) -> Self {
165        Self {
166            plan: Some(plan),
167            behavior: Some(behavior),
168            feature: None,
169        }
170    }
171
172    pub fn with_feature(mut self, feature: &'a str) -> Self {
173        self.feature = Some(feature);
174        self
175    }
176}
177
178/// Parameters for setting a threshold (stub - not yet implemented)
179#[derive(Debug, Clone, Default)]
180pub struct SetThresholdParams {
181    pub threshold_id: i64,
182}
183
184impl<'a> TryFrom<&'a Value> for DeleteGenericThresholdParams<'a> {
185    type Error = SzConfigError;
186
187    fn try_from(json: &'a Value) -> Result<Self> {
188        Ok(Self {
189            plan: json.get("plan").and_then(|v| v.as_str()),
190            behavior: json.get("behavior").and_then(|v| v.as_str()),
191            feature: json.get("feature").and_then(|v| v.as_str()),
192        })
193    }
194}
195
196// ===== Comparison Thresholds (CFG_CFRTN) =====
197
198/// Add a new comparison threshold (CFG_CFRTN record)
199///
200/// # Arguments
201/// * `config_json` - JSON configuration string
202/// * `params` - Threshold parameters (cfunc_id, cfunc_rtnval required; others optional)
203///
204/// # Returns
205/// Modified configuration JSON string
206pub fn add_comparison_threshold(
207    config_json: &str,
208    params: AddComparisonThresholdParams,
209) -> Result<String> {
210    // Extract and validate required fields
211    let cfunc_code = params
212        .cfunc_code
213        .ok_or_else(|| SzConfigError::MissingField("cfunc_code".to_string()))?;
214    let ftype_code = params
215        .ftype_code
216        .ok_or_else(|| SzConfigError::MissingField("ftype_code".to_string()))?;
217    let cfunc_rtnval = params
218        .cfunc_rtnval
219        .ok_or_else(|| SzConfigError::MissingField("cfunc_rtnval".to_string()))?;
220
221    // Lookup IDs from codes (special case: "all" = ftype_id 0)
222    let cfunc_id = helpers::lookup_cfunc_id(config_json, cfunc_code)?;
223    let ftype_id = if ftype_code.eq_ignore_ascii_case("all") {
224        0 // Special case: "all" means ftype_id=0 (all features)
225    } else {
226        helpers::lookup_feature_id(config_json, ftype_code)?
227    };
228
229    let config: Value =
230        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
231
232    let rtnval_upper = cfunc_rtnval.to_uppercase();
233
234    // Check if already exists
235    let cfrtn_array = config["G2_CONFIG"]["CFG_CFRTN"]
236        .as_array()
237        .ok_or_else(|| SzConfigError::MissingSection("CFG_CFRTN".to_string()))?;
238
239    if cfrtn_array.iter().any(|item| {
240        item["CFUNC_ID"].as_i64() == Some(cfunc_id)
241            && item["FTYPE_ID"].as_i64() == Some(ftype_id)
242            && item["CFUNC_RTNVAL"].as_str() == Some(rtnval_upper.as_str())
243    }) {
244        return Err(SzConfigError::AlreadyExists(format!(
245            "Comparison threshold: {cfunc_code}+{ftype_code}+{rtnval_upper}"
246        )));
247    }
248
249    // Get next ID
250    let cfrtn_id = helpers::get_next_id_from_array(cfrtn_array, "CFRTN_ID")?;
251
252    // Build record
253    let mut record = json!({
254        "CFRTN_ID": cfrtn_id,
255        "CFUNC_ID": cfunc_id,
256        "FTYPE_ID": ftype_id,
257        "CFUNC_RTNVAL": rtnval_upper,
258    });
259
260    record["EXEC_ORDER"] = match params.exec_order {
261        Some(order) => json!(order),
262        None => Value::Null,
263    };
264    record["SAME_SCORE"] = match params.same_score {
265        Some(score) => json!(score),
266        None => Value::Null,
267    };
268    record["CLOSE_SCORE"] = match params.close_score {
269        Some(score) => json!(score),
270        None => Value::Null,
271    };
272    record["LIKELY_SCORE"] = match params.likely_score {
273        Some(score) => json!(score),
274        None => Value::Null,
275    };
276    record["PLAUSIBLE_SCORE"] = match params.plausible_score {
277        Some(score) => json!(score),
278        None => Value::Null,
279    };
280    record["UN_LIKELY_SCORE"] = match params.un_likely_score {
281        Some(score) => json!(score),
282        None => Value::Null,
283    };
284
285    helpers::add_to_config_array(config_json, "CFG_CFRTN", record)
286}
287
288/// Internal: Add comparison threshold by ID (for FFI use)
289#[allow(clippy::too_many_arguments)]
290pub(crate) fn add_comparison_threshold_by_id(
291    config_json: &str,
292    cfunc_id: i64,
293    ftype_id: Option<i64>,
294    cfunc_rtnval: &str,
295    exec_order: Option<i64>,
296    same_score: Option<i64>,
297    close_score: Option<i64>,
298    likely_score: Option<i64>,
299    plausible_score: Option<i64>,
300    un_likely_score: Option<i64>,
301) -> Result<String> {
302    let config: Value =
303        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
304
305    let ftype = ftype_id.unwrap_or(0);
306    let rtnval_upper = cfunc_rtnval.to_uppercase();
307
308    // Check if already exists
309    let cfrtn_array = config["G2_CONFIG"]["CFG_CFRTN"]
310        .as_array()
311        .ok_or_else(|| SzConfigError::MissingSection("CFG_CFRTN".to_string()))?;
312
313    if cfrtn_array.iter().any(|item| {
314        item["CFUNC_ID"].as_i64() == Some(cfunc_id)
315            && item["FTYPE_ID"].as_i64() == Some(ftype)
316            && item["CFUNC_RTNVAL"].as_str() == Some(rtnval_upper.as_str())
317    }) {
318        return Err(SzConfigError::AlreadyExists(
319            "Comparison threshold already exists".to_string(),
320        ));
321    }
322
323    // Get next ID
324    let cfrtn_id = crate::helpers::get_next_id_from_array(cfrtn_array, "CFRTN_ID")?;
325
326    // Build record
327    let mut record = json!({
328        "CFRTN_ID": cfrtn_id,
329        "CFUNC_ID": cfunc_id,
330        "FTYPE_ID": ftype,
331        "CFUNC_RTNVAL": rtnval_upper,
332    });
333
334    record["EXEC_ORDER"] = match exec_order {
335        Some(order) => json!(order),
336        None => Value::Null,
337    };
338    record["SAME_SCORE"] = match same_score {
339        Some(score) => json!(score),
340        None => Value::Null,
341    };
342    record["CLOSE_SCORE"] = match close_score {
343        Some(score) => json!(score),
344        None => Value::Null,
345    };
346    record["LIKELY_SCORE"] = match likely_score {
347        Some(score) => json!(score),
348        None => Value::Null,
349    };
350    record["PLAUSIBLE_SCORE"] = match plausible_score {
351        Some(score) => json!(score),
352        None => Value::Null,
353    };
354    record["UN_LIKELY_SCORE"] = match un_likely_score {
355        Some(score) => json!(score),
356        None => Value::Null,
357    };
358
359    crate::helpers::add_to_config_array(config_json, "CFG_CFRTN", record)
360}
361
362/// Internal: Set comparison threshold by ID (for FFI use)
363pub(crate) fn set_comparison_threshold_by_id(
364    config_json: &str,
365    cfrtn_id: i64,
366    same_score: Option<i64>,
367    close_score: Option<i64>,
368    likely_score: Option<i64>,
369    plausible_score: Option<i64>,
370    un_likely_score: Option<i64>,
371) -> Result<String> {
372    let mut config: Value =
373        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
374
375    let cfrtn_array = config["G2_CONFIG"]["CFG_CFRTN"]
376        .as_array_mut()
377        .ok_or_else(|| SzConfigError::MissingSection("CFG_CFRTN".to_string()))?;
378
379    let cfrtn = cfrtn_array
380        .iter_mut()
381        .find(|item| item["CFRTN_ID"].as_i64() == Some(cfrtn_id))
382        .ok_or_else(|| SzConfigError::NotFound(format!("Comparison threshold ID: {cfrtn_id}")))?;
383
384    // Update fields from params
385    if let Some(dest_obj) = cfrtn.as_object_mut() {
386        if let Some(score) = same_score {
387            dest_obj.insert("SAME_SCORE".to_string(), json!(score));
388        }
389        if let Some(score) = close_score {
390            dest_obj.insert("CLOSE_SCORE".to_string(), json!(score));
391        }
392        if let Some(score) = likely_score {
393            dest_obj.insert("LIKELY_SCORE".to_string(), json!(score));
394        }
395        if let Some(score) = plausible_score {
396            dest_obj.insert("PLAUSIBLE_SCORE".to_string(), json!(score));
397        }
398        if let Some(score) = un_likely_score {
399            dest_obj.insert("UN_LIKELY_SCORE".to_string(), json!(score));
400        }
401    }
402
403    serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
404}
405
406/// Internal: Delete comparison threshold by ID (for FFI use)
407///
408/// # Arguments
409/// * `config_json` - JSON configuration string
410/// * `cfrtn_id` - Comparison threshold ID
411pub(crate) fn delete_comparison_threshold_by_id(
412    config_json: &str,
413    cfrtn_id: i64,
414) -> Result<String> {
415    let mut config: Value =
416        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
417
418    let mut found = false;
419
420    if let Some(cfrtn_array) = config["G2_CONFIG"]["CFG_CFRTN"].as_array_mut() {
421        cfrtn_array.retain(|item| {
422            let matches = item["CFRTN_ID"].as_i64() == Some(cfrtn_id);
423            if matches {
424                found = true;
425            }
426            !matches
427        });
428    }
429
430    if !found {
431        return Err(SzConfigError::NotFound(format!(
432            "Comparison threshold ID: {cfrtn_id}"
433        )));
434    }
435
436    serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
437}
438
439pub fn delete_comparison_threshold(
440    config_json: &str,
441    cfunc_code: &str,
442    ftype_code: &str,
443) -> Result<String> {
444    let cfunc_id = helpers::lookup_cfunc_id(config_json, cfunc_code)?;
445    let ftype_id = helpers::lookup_feature_id(config_json, ftype_code)?;
446
447    // Find the CFRTN_ID for this combination
448    let config_lookup: Value =
449        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
450
451    let cfrtn_array = config_lookup["G2_CONFIG"]["CFG_CFRTN"]
452        .as_array()
453        .ok_or_else(|| SzConfigError::MissingSection("CFG_CFRTN".to_string()))?;
454
455    let cfrtn_id = cfrtn_array
456        .iter()
457        .find(|item| {
458            item["CFUNC_ID"].as_i64() == Some(cfunc_id)
459                && item["FTYPE_ID"].as_i64() == Some(ftype_id)
460        })
461        .and_then(|item| item["CFRTN_ID"].as_i64())
462        .ok_or_else(|| {
463            SzConfigError::NotFound(format!(
464                "Comparison threshold for cfunc='{cfunc_code}', ftype='{ftype_code}'"
465            ))
466        })?;
467
468    let mut config: Value =
469        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
470
471    let mut found = false;
472
473    if let Some(cfrtn_array) = config["G2_CONFIG"]["CFG_CFRTN"].as_array_mut() {
474        cfrtn_array.retain(|item| {
475            let matches = item["CFRTN_ID"].as_i64() == Some(cfrtn_id);
476            if matches {
477                found = true;
478            }
479            !matches
480        });
481    }
482
483    if !found {
484        return Err(SzConfigError::NotFound(format!(
485            "Comparison threshold: {cfrtn_id}"
486        )));
487    }
488
489    serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
490}
491
492/// Set (update) a comparison threshold
493///
494/// # Arguments
495/// * `config_json` - JSON configuration string
496/// * `params` - Threshold parameters (cfrtn_id required; score fields optional)
497///
498/// # Returns
499/// Modified configuration JSON string
500pub fn set_comparison_threshold(
501    config_json: &str,
502    params: SetComparisonThresholdParams,
503) -> Result<String> {
504    // Extract and validate required fields
505    let cfunc_code = params
506        .cfunc_code
507        .ok_or_else(|| SzConfigError::MissingField("cfunc_code".to_string()))?;
508    let ftype_code = params
509        .ftype_code
510        .ok_or_else(|| SzConfigError::MissingField("ftype_code".to_string()))?;
511    let cfunc_rtnval = params
512        .cfunc_rtnval
513        .ok_or_else(|| SzConfigError::MissingField("cfunc_rtnval".to_string()))?;
514
515    // Lookup IDs from codes (special case: "all" = ftype_id 0)
516    let cfunc_id = helpers::lookup_cfunc_id(config_json, cfunc_code)?;
517    let ftype_id = if ftype_code.eq_ignore_ascii_case("all") {
518        0 // Special case: "all" means ftype_id=0 (all features)
519    } else {
520        helpers::lookup_feature_id(config_json, ftype_code)?
521    };
522
523    let mut config: Value =
524        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
525
526    let cfrtn_array = config["G2_CONFIG"]["CFG_CFRTN"]
527        .as_array_mut()
528        .ok_or_else(|| SzConfigError::MissingSection("CFG_CFRTN".to_string()))?;
529
530    // Find threshold by (CFUNC_ID, FTYPE_ID, CFUNC_RTNVAL) - all 3 needed for uniqueness
531    let cfrtn = cfrtn_array
532        .iter_mut()
533        .find(|item| {
534            item["CFUNC_ID"].as_i64() == Some(cfunc_id)
535                && item["FTYPE_ID"].as_i64() == Some(ftype_id)
536                && item["CFUNC_RTNVAL"]
537                    .as_str()
538                    .map(|s| s.eq_ignore_ascii_case(cfunc_rtnval))
539                    .unwrap_or(false)
540        })
541        .ok_or_else(|| {
542            SzConfigError::NotFound(format!(
543                "Comparison threshold: {cfunc_code}+{ftype_code}+{cfunc_rtnval}"
544            ))
545        })?;
546
547    // Update fields from params
548    if let Some(dest_obj) = cfrtn.as_object_mut() {
549        if let Some(order) = params.exec_order {
550            dest_obj.insert("EXEC_ORDER".to_string(), json!(order));
551        }
552        if let Some(score) = params.same_score {
553            dest_obj.insert("SAME_SCORE".to_string(), json!(score));
554        }
555        if let Some(score) = params.close_score {
556            dest_obj.insert("CLOSE_SCORE".to_string(), json!(score));
557        }
558        if let Some(score) = params.likely_score {
559            dest_obj.insert("LIKELY_SCORE".to_string(), json!(score));
560        }
561        if let Some(score) = params.plausible_score {
562            dest_obj.insert("PLAUSIBLE_SCORE".to_string(), json!(score));
563        }
564        if let Some(score) = params.un_likely_score {
565            dest_obj.insert("UN_LIKELY_SCORE".to_string(), json!(score));
566        }
567    }
568
569    serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
570}
571
572/// List all comparison thresholds with resolved names
573///
574/// # Arguments
575/// * `config_json` - JSON configuration string
576///
577/// # Returns
578/// Vector of JSON Values with id, function, returnOrder, scoreName, feature, and score fields
579pub fn list_comparison_thresholds(config_json: &str) -> Result<Vec<Value>> {
580    let config: Value =
581        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
582
583    let cfrtn_array = config["G2_CONFIG"]["CFG_CFRTN"]
584        .as_array()
585        .ok_or_else(|| SzConfigError::MissingSection("CFG_CFRTN".to_string()))?;
586
587    let cfunc_array = config["G2_CONFIG"]["CFG_CFUNC"]
588        .as_array()
589        .ok_or_else(|| SzConfigError::MissingSection("CFG_CFUNC".to_string()))?;
590
591    let ftype_array = config["G2_CONFIG"]["CFG_FTYPE"]
592        .as_array()
593        .ok_or_else(|| SzConfigError::MissingSection("CFG_FTYPE".to_string()))?;
594
595    let mut result: Vec<Value> = cfrtn_array
596        .iter()
597        .map(|item| {
598            let cfunc_id = item["CFUNC_ID"].as_i64().unwrap_or(0);
599            let ftype_id = item["FTYPE_ID"].as_i64().unwrap_or(0);
600            let cfrtn_id = item["CFRTN_ID"].as_i64().unwrap_or(0);
601
602            // Resolve function name
603            let function = cfunc_array
604                .iter()
605                .find(|cf| cf["CFUNC_ID"].as_i64() == Some(cfunc_id))
606                .and_then(|cf| cf["CFUNC_CODE"].as_str())
607                .unwrap_or("unknown")
608                .to_string();
609
610            // Resolve feature name
611            let feature = if ftype_id == 0 {
612                "all".to_string()
613            } else {
614                ftype_array
615                    .iter()
616                    .find(|ft| ft["FTYPE_ID"].as_i64() == Some(ftype_id))
617                    .and_then(|ft| ft["FTYPE_CODE"].as_str())
618                    .unwrap_or("unknown")
619                    .to_string()
620            };
621
622            json!({
623                "id": cfrtn_id,
624                "cfunc_id": cfunc_id,  // Keep for sorting
625                "function": function,
626                "returnOrder": item["EXEC_ORDER"].as_i64().unwrap_or(0),
627                "scoreName": item["CFUNC_RTNVAL"].as_str().unwrap_or(""),
628                "feature": feature,
629                "sameScore": item["SAME_SCORE"].as_i64().unwrap_or(0),
630                "closeScore": item["CLOSE_SCORE"].as_i64().unwrap_or(0),
631                "likelyScore": item["LIKELY_SCORE"].as_i64().unwrap_or(0),
632                "plausibleScore": item["PLAUSIBLE_SCORE"].as_i64().unwrap_or(0),
633                "unlikelyScore": item["UN_LIKELY_SCORE"].as_i64().unwrap_or(0)
634            })
635        })
636        .collect();
637
638    // Sort by CFUNC_ID and CFRTN_ID (like Python) - not by function name
639    result.sort_by_key(|e| {
640        (
641            e["cfunc_id"].as_i64().unwrap_or(0),
642            e["id"].as_i64().unwrap_or(0),
643        )
644    });
645
646    // Rebuild output with correct field order (remove cfunc_id and ensure proper order)
647    let final_result: Vec<Value> = result
648        .iter()
649        .map(|item| {
650            json!({
651                "id": item["id"],
652                "function": item["function"],
653                "returnOrder": item["returnOrder"],
654                "scoreName": item["scoreName"],
655                "feature": item["feature"],
656                "sameScore": item["sameScore"],
657                "closeScore": item["closeScore"],
658                "likelyScore": item["likelyScore"],
659                "plausibleScore": item["plausibleScore"],
660                "unlikelyScore": item["unlikelyScore"]
661            })
662        })
663        .collect();
664
665    Ok(final_result)
666}
667
668// ===== Generic Thresholds (CFG_GENERIC_THRESHOLD) =====
669
670/// Add a new generic threshold (CFG_GENERIC_THRESHOLD record)
671///
672/// # Arguments
673/// * `config_json` - JSON configuration string
674/// * `params` - Generic threshold parameters (plan, behavior, caps required; feature optional)
675///
676/// # Returns
677/// Modified configuration JSON string
678pub fn add_generic_threshold(
679    config_json: &str,
680    params: AddGenericThresholdParams,
681) -> Result<String> {
682    // Extract and validate required fields
683    let plan = params
684        .plan
685        .ok_or_else(|| SzConfigError::MissingField("plan".to_string()))?;
686    let behavior = params
687        .behavior
688        .ok_or_else(|| SzConfigError::MissingField("behavior".to_string()))?;
689    let scoring_cap = params
690        .scoring_cap
691        .ok_or_else(|| SzConfigError::MissingField("scoring_cap".to_string()))?;
692    let candidate_cap = params
693        .candidate_cap
694        .ok_or_else(|| SzConfigError::MissingField("candidate_cap".to_string()))?;
695    let send_to_redo = params
696        .send_to_redo
697        .ok_or_else(|| SzConfigError::MissingField("send_to_redo".to_string()))?;
698
699    let mut config: Value =
700        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
701
702    let plan_upper = plan.to_uppercase();
703    let behavior_upper = behavior.to_uppercase();
704    let redo_upper = send_to_redo.to_uppercase();
705    let feature_upper = params.feature.unwrap_or("ALL").to_uppercase();
706
707    // Validate sendToRedo
708    if redo_upper != "YES" && redo_upper != "NO" {
709        return Err(SzConfigError::InvalidInput(format!(
710            "Invalid sendToRedo value '{send_to_redo}'. Must be 'Yes' or 'No'"
711        )));
712    }
713
714    // Lookup plan ID
715    let gplan_array = config["G2_CONFIG"]["CFG_GPLAN"]
716        .as_array()
717        .ok_or_else(|| SzConfigError::MissingSection("CFG_GPLAN".to_string()))?;
718
719    let gplan_id = gplan_array
720        .iter()
721        .find(|p| p["GPLAN_CODE"].as_str() == Some(plan_upper.as_str()))
722        .and_then(|p| p["GPLAN_ID"].as_i64())
723        .ok_or_else(|| SzConfigError::NotFound(format!("Generic plan: {}", plan_upper.clone())))?;
724
725    // Lookup feature ID (0 for "all")
726    let ftype_id = if feature_upper == "ALL" {
727        0
728    } else {
729        let ftype_array = config["G2_CONFIG"]["CFG_FTYPE"]
730            .as_array()
731            .ok_or_else(|| SzConfigError::MissingSection("CFG_FTYPE".to_string()))?;
732
733        ftype_array
734            .iter()
735            .find(|f| f["FTYPE_CODE"].as_str() == Some(feature_upper.as_str()))
736            .and_then(|f| f["FTYPE_ID"].as_i64())
737            .ok_or_else(|| SzConfigError::NotFound(format!("Feature: {}", feature_upper.clone())))?
738    };
739
740    // Check if threshold already exists
741    let gthresh_array = config["G2_CONFIG"]["CFG_GENERIC_THRESHOLD"]
742        .as_array()
743        .ok_or_else(|| SzConfigError::MissingSection("CFG_GENERIC_THRESHOLD".to_string()))?;
744
745    if gthresh_array.iter().any(|record| {
746        record["GPLAN_ID"].as_i64() == Some(gplan_id)
747            && record["BEHAVIOR"].as_str() == Some(behavior_upper.as_str())
748            && record["FTYPE_ID"].as_i64() == Some(ftype_id)
749    }) {
750        return Err(SzConfigError::AlreadyExists(format!(
751            "Generic threshold: plan={plan_upper}, behavior={behavior_upper}, feature={feature_upper}"
752        )));
753    }
754
755    // Create new threshold record
756    let new_threshold = json!({
757        "GPLAN_ID": gplan_id,
758        "BEHAVIOR": behavior_upper,
759        "FTYPE_ID": ftype_id,
760        "CANDIDATE_CAP": candidate_cap,
761        "SCORING_CAP": scoring_cap,
762        "SEND_TO_REDO": redo_upper
763    });
764
765    if let Some(threshold_array) = config["G2_CONFIG"]["CFG_GENERIC_THRESHOLD"].as_array_mut() {
766        threshold_array.push(new_threshold);
767    }
768
769    serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
770}
771
772/// Delete a generic threshold
773///
774/// # Arguments
775/// * `config_json` - JSON configuration string
776/// * `params` - Delete parameters (gplan_id, behavior required; feature optional)
777///
778/// # Returns
779/// Modified configuration JSON string
780pub fn delete_generic_threshold(
781    config_json: &str,
782    params: DeleteGenericThresholdParams,
783) -> Result<String> {
784    // Extract and validate required fields
785    let plan = params
786        .plan
787        .ok_or_else(|| SzConfigError::MissingField("plan".to_string()))?;
788    let behavior = params
789        .behavior
790        .ok_or_else(|| SzConfigError::MissingField("behavior".to_string()))?;
791
792    let gplan_id = helpers::lookup_gplan_id(config_json, plan)?;
793
794    let mut config: Value =
795        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
796
797    let behavior_upper = behavior.to_uppercase();
798    let feature_upper = params.feature.unwrap_or("ALL").to_uppercase();
799
800    // Lookup feature ID (0 for "all")
801    let ftype_id = if feature_upper == "ALL" {
802        0
803    } else {
804        let ftype_array = config["G2_CONFIG"]["CFG_FTYPE"]
805            .as_array()
806            .ok_or_else(|| SzConfigError::MissingSection("CFG_FTYPE".to_string()))?;
807
808        ftype_array
809            .iter()
810            .find(|f| f["FTYPE_CODE"].as_str() == Some(feature_upper.as_str()))
811            .and_then(|f| f["FTYPE_ID"].as_i64())
812            .ok_or_else(|| SzConfigError::NotFound(format!("Feature: {}", feature_upper.clone())))?
813    };
814
815    // Find and delete threshold record
816    let mut found = false;
817    if let Some(threshold_array) = config["G2_CONFIG"]["CFG_GENERIC_THRESHOLD"].as_array_mut() {
818        threshold_array.retain(|record| {
819            let matches = record["GPLAN_ID"].as_i64() == Some(gplan_id)
820                && record["BEHAVIOR"].as_str() == Some(behavior_upper.as_str())
821                && record["FTYPE_ID"].as_i64() == Some(ftype_id);
822            if matches {
823                found = true;
824            }
825            !matches
826        });
827    }
828
829    if !found {
830        return Err(SzConfigError::NotFound(format!(
831            "Generic threshold not found: GPLAN_ID={gplan_id}, behavior={behavior_upper}, feature={feature_upper}"
832        )));
833    }
834
835    serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
836}
837
838/// Set (update) a generic threshold
839///
840/// # Arguments
841/// * `config_json` - JSON configuration string
842/// * `params` - Threshold parameters (gplan_id, behavior required; caps/redo optional)
843///
844/// # Returns
845/// Modified configuration JSON string
846pub fn set_generic_threshold(
847    config_json: &str,
848    params: SetGenericThresholdParams,
849) -> Result<String> {
850    // Extract and validate required fields
851    let plan = params
852        .plan
853        .ok_or_else(|| SzConfigError::MissingField("plan".to_string()))?;
854    let behavior = params
855        .behavior
856        .ok_or_else(|| SzConfigError::MissingField("behavior".to_string()))?;
857
858    let gplan_id = helpers::lookup_gplan_id(config_json, plan)?;
859
860    let mut config: Value =
861        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
862
863    let behavior_upper = behavior.to_uppercase();
864
865    let gthresh_array = config["G2_CONFIG"]["CFG_GENERIC_THRESHOLD"]
866        .as_array_mut()
867        .ok_or_else(|| SzConfigError::MissingSection("CFG_GENERIC_THRESHOLD".to_string()))?;
868
869    let gthresh = gthresh_array
870        .iter_mut()
871        .find(|item| {
872            item["GPLAN_ID"].as_i64() == Some(gplan_id)
873                && item["BEHAVIOR"].as_str() == Some(behavior_upper.as_str())
874        })
875        .ok_or_else(|| {
876            SzConfigError::NotFound(format!(
877                "Generic threshold not found: GPLAN_ID={gplan_id}, BEHAVIOR={behavior_upper}"
878            ))
879        })?;
880
881    // Update fields from params
882    if let Some(dest_obj) = gthresh.as_object_mut() {
883        if let Some(feature_code) = params.feature {
884            let new_ftype_id = helpers::lookup_feature_id(config_json, feature_code)?;
885            dest_obj.insert("FTYPE_ID".to_string(), json!(new_ftype_id));
886        }
887        if let Some(cap) = params.candidate_cap {
888            dest_obj.insert("CANDIDATE_CAP".to_string(), json!(cap));
889        }
890        if let Some(cap) = params.scoring_cap {
891            dest_obj.insert("SCORING_CAP".to_string(), json!(cap));
892        }
893        if let Some(redo) = params.send_to_redo {
894            dest_obj.insert("SEND_TO_REDO".to_string(), json!(redo.to_uppercase()));
895        }
896    }
897
898    serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
899}
900
901/// List all generic thresholds with resolved names
902///
903/// # Arguments
904/// * `config_json` - JSON configuration string
905///
906/// # Returns
907/// Vector of JSON Values with plan, behavior, feature, candidateCap, scoringCap, and sendToRedo fields
908pub fn list_generic_thresholds(config_json: &str) -> Result<Vec<Value>> {
909    let config: Value =
910        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
911
912    let gthresh_array = config["G2_CONFIG"]["CFG_GENERIC_THRESHOLD"]
913        .as_array()
914        .ok_or_else(|| SzConfigError::MissingSection("CFG_GENERIC_THRESHOLD".to_string()))?;
915
916    let gplan_array = config["G2_CONFIG"]["CFG_GPLAN"]
917        .as_array()
918        .ok_or_else(|| SzConfigError::MissingSection("CFG_GPLAN".to_string()))?;
919
920    let ftype_array = config["G2_CONFIG"]["CFG_FTYPE"]
921        .as_array()
922        .ok_or_else(|| SzConfigError::MissingSection("CFG_FTYPE".to_string()))?;
923
924    let result: Vec<Value> = gthresh_array
925        .iter()
926        .map(|item| {
927            let gplan_id = item["GPLAN_ID"].as_i64().unwrap_or(0);
928            let ftype_id = item["FTYPE_ID"].as_i64().unwrap_or(0);
929
930            // Resolve plan name
931            let plan = gplan_array
932                .iter()
933                .find(|gp| gp["GPLAN_ID"].as_i64() == Some(gplan_id))
934                .and_then(|gp| gp["GPLAN_CODE"].as_str())
935                .unwrap_or("unknown")
936                .to_string();
937
938            // Resolve feature name
939            let feature = if ftype_id == 0 {
940                "all".to_string()
941            } else {
942                ftype_array
943                    .iter()
944                    .find(|ft| ft["FTYPE_ID"].as_i64() == Some(ftype_id))
945                    .and_then(|ft| ft["FTYPE_CODE"].as_str())
946                    .unwrap_or("unknown")
947                    .to_string()
948            };
949
950            json!({
951                "plan": plan,
952                "behavior": item["BEHAVIOR"].as_str().unwrap_or(""),
953                "feature": feature,
954                "candidateCap": item["CANDIDATE_CAP"].as_i64().unwrap_or(0),
955                "scoringCap": item["SCORING_CAP"].as_i64().unwrap_or(0),
956                "sendToRedo": item["SEND_TO_REDO"].as_str().unwrap_or("")
957            })
958        })
959        .collect();
960
961    Ok(result)
962}
963
964/// Get threshold level by ID
965///
966/// This is a placeholder for get_threshold() functionality.
967/// TODO: Determine exact requirements for this function.
968///
969/// # Arguments
970/// * `config_json` - JSON configuration string
971/// * `threshold_id` - Threshold ID
972///
973/// # Returns
974/// JSON Value representing the threshold
975pub fn get_threshold(_config_json: &str, _threshold_id: i64) -> Result<Value> {
976    Err(SzConfigError::InvalidInput(
977        "get_threshold not yet implemented".to_string(),
978    ))
979}
980
981/// Set threshold level by ID
982///
983/// This is a placeholder for set_threshold() functionality.
984/// TODO: Determine exact requirements for this function.
985///
986/// # Arguments
987/// * `config_json` - JSON configuration string
988/// * `params` - Threshold parameters (threshold_id required to identify, others optional to update)
989///
990/// # Returns
991/// Modified configuration JSON string
992pub fn set_threshold(_config_json: &str, _params: SetThresholdParams) -> Result<String> {
993    Err(SzConfigError::InvalidInput(
994        "set_threshold not yet implemented".to_string(),
995    ))
996}