package lib import ( "encoding/json" "fmt" "log" "strings" ) // Normalize resolves hospital test names to official LOINC codes and updates entries. // Flow: hospital name + specimen + unit → LOINC (via cache or Gemini lookup) → official name from loinc_lab. // No Fireworks LLM. Original Type field is never modified. func Normalize(dossierID string, category int, progress ...func(processed, total int)) error { reportProgress := func(p, t int) { if len(progress) > 0 && progress[0] != nil { progress[0](p, t) } } // 1. Load all entries, build parent map for specimen lookup entries, err := EntryQueryOld(dossierID, category, "") if err != nil { return fmt.Errorf("load entries: %w", err) } parentMap := make(map[string]*Entry) for _, e := range entries { if e.ParentID == "" || e.Type == "lab_order" { parentMap[e.EntryID] = e } } // 2. Collect unique type|specimen|unit combos, resolve each to LOINC type testKey struct { name, specimen, unit string } type resolved struct { loinc string info *LoincInfo abbr string } cache := make(map[testKey]*resolved) var lookupCount, cacheHits, misses int for _, e := range entries { if e.ParentID == "" || e.Type == "lab_order" || e.Type == "" { continue } var data map[string]interface{} json.Unmarshal([]byte(e.Data), &data) unit, _ := data["unit"].(string) specimen := "" if parent, ok := parentMap[e.ParentID]; ok { var pdata map[string]interface{} if json.Unmarshal([]byte(parent.Data), &pdata) == nil { specimen, _ = pdata["specimen"].(string) } } tk := testKey{e.Type, specimen, unit} if _, ok := cache[tk]; ok { continue } lookupCount++ loinc := LoincLookup(e.Type, specimen, unit) if loinc == "" { cache[tk] = &resolved{} misses++ continue } info := LoincGet(loinc) abbr := LoincAbbr(info) cache[tk] = &resolved{loinc: loinc, info: info, abbr: abbr} if info != nil { cacheHits++ } } reportProgress(lookupCount, lookupCount) log.Printf("normalize: %d unique combos, %d resolved, %d unresolved", lookupCount, cacheHits, misses) // 3. Apply to entries var toSave []Entry for _, e := range entries { if e.ParentID == "" || e.Type == "lab_order" || e.Type == "" { continue } var data map[string]interface{} if json.Unmarshal([]byte(e.Data), &data) != nil { data = make(map[string]interface{}) } unit, _ := data["unit"].(string) specimen := "" if parent, ok := parentMap[e.ParentID]; ok { var pdata map[string]interface{} if json.Unmarshal([]byte(parent.Data), &pdata) == nil { specimen, _ = pdata["specimen"].(string) } } r := cache[testKey{e.Type, specimen, unit}] if r == nil || r.loinc == "" || r.info == nil { continue } // Check if already up to date existingLoinc, _ := data["loinc"].(string) existingName, _ := data["normalized_name"].(string) if existingLoinc == r.loinc && existingName == r.info.LongName && e.SearchKey == r.loinc && e.SearchKey2 != "" { continue } // Update Data JSON data["loinc"] = r.loinc data["normalized_name"] = r.info.LongName data["abbreviation"] = r.abbr b, _ := json.Marshal(data) e.Data = string(b) // Update search keys e.SearchKey = r.loinc e.SearchKey2 = strings.ToLower(r.info.LongName) // Rebuild Summary: "Abbr: value unit" summary := r.abbr + ": " + e.Value if unit != "" { summary += " " + unit } e.Summary = summary toSave = append(toSave, *e) } if len(toSave) == 0 { log.Printf("normalize: no changes needed") return nil } log.Printf("normalize: updating %d entries", len(toSave)) ptrs := make([]*Entry, len(toSave)) for i := range toSave { ptrs[i] = &toSave[i] } return EntryWrite("", ptrs...) }