package catalog import ( "fmt" "strings" "github.com/descrybe/descrybe-v2/apps/api/internal/feeds" ) // linkFeedSpecificationsIntoProduct expands mapped_data.specifications HTML/flat // blobs into attribute_key→value maps and merges them into attributes when empty. // Mutates item in place for GET product responses (does not persist). func linkFeedSpecificationsIntoProduct(item map[string]any) { if item == nil { return } mapped, _ := item["mapped_data"].(map[string]any) if mapped == nil { return } linked := extractLinkedSpecs(mapped) if len(linked) == 0 { return } // Prefer structured object in response mapped_data for the Attributes/Feed UI. if specs := mapped["specifications"]; isLooseSpecBlob(specs) { mapped["specifications"] = linked item["mapped_data"] = mapped } else if specs := mapped["specs"]; isLooseSpecBlob(specs) { mapped["specs"] = linked item["mapped_data"] = mapped } attrs := asStringAnyMap(item["attributes"]) if len(attrs) == 0 { item["attributes"] = linked item["has_attributes"] = true return } merged := make(map[string]any, len(attrs)+len(linked)) for k, v := range attrs { merged[k] = v } for k, v := range linked { if existing, ok := merged[k]; ok && strings.TrimSpace(stringifyAny(existing)) != "" { continue } merged[k] = v } item["attributes"] = merged item["has_attributes"] = true } func extractLinkedSpecs(mapped map[string]any) map[string]any { out := map[string]any{} put := func(label, value string) { k := feeds.CanonicalAttributeKey(label) if k == "" || strings.TrimSpace(value) == "" { return } if _, exists := out[k]; exists { return } out[k] = value } for _, key := range []string{"specifications", "specs"} { v, ok := mapped[key] if !ok || v == nil { continue } switch t := v.(type) { case string: for _, p := range feeds.ParseSpecifications(t) { put(p.Label, p.Value) } case map[string]any: for k, val := range t { if strings.EqualFold(k, "_raw") { if s, ok := val.(string); ok { for _, p := range feeds.ParseSpecifications(s) { put(p.Label, p.Value) } } continue } put(k, stringifyAny(val)) } case map[string]string: for k, val := range t { if strings.EqualFold(k, "_raw") { for _, p := range feeds.ParseSpecifications(val) { put(p.Label, p.Value) } continue } put(k, val) } } } // Scalar mapped fields → standard attribute keys (net_height, eprel_id, …). for _, src := range []string{ "warranty", "eprel_id", "eprel", "netwidth", "net_width", "netheight", "net_height", "netdepth", "net_depth", "netmass", "net_mass", "productmodel", "product_model", "visina", "sirina", "globina", "teza", } { if s := stringifyAny(mapped[src]); s != "" { put(src, s) } } if len(out) == 0 { return nil } return out } func isLooseSpecBlob(v any) bool { switch t := v.(type) { case string: return strings.TrimSpace(t) != "" case map[string]any: _, hasRaw := t["_raw"] return hasRaw case map[string]string: _, hasRaw := t["_raw"] return hasRaw default: return false } } func asStringAnyMap(v any) map[string]any { switch t := v.(type) { case map[string]any: return t case map[string]string: out := make(map[string]any, len(t)) for k, val := range t { out[k] = val } return out default: return nil } } func stringifyAny(v any) string { if v == nil { return "" } switch t := v.(type) { case string: return strings.TrimSpace(t) case float64, float32, int, int64, bool: return strings.TrimSpace(fmt.Sprint(t)) default: return strings.TrimSpace(fmt.Sprint(t)) } }