package ingest import ( "context" "database/sql" "fmt" "regexp" "strconv" "strings" "reanimator/internal/idgen" ) const ( mysqlErrDuplicateKey = 1062 ) var lotNormalizePattern = regexp.MustCompile(`[^0-9A-Z]+`) func (s *Service) ensureLotByCode(ctx context.Context, tx *sql.Tx, code string, description *string) (string, error) { if code == "" { return "", nil } var id string err := tx.QueryRowContext(ctx, `SELECT id FROM lots WHERE code = ? LIMIT 1`, code, ).Scan(&id) if err == nil { return id, nil } if err != sql.ErrNoRows { return "", err } var descriptionValue interface{} if description != nil { descriptionValue = *description } lotID, err := s.idgen.Generate(ctx, idgen.Lot) if err != nil { return "", err } _, err = tx.ExecContext(ctx, `INSERT INTO lots (id, code, description) VALUES (?, ?, ?)`, lotID, code, descriptionValue, ) if err != nil { if mysqlErrorNumber(err) == mysqlErrDuplicateKey { if err := tx.QueryRowContext(ctx, `SELECT id FROM lots WHERE code = ? LIMIT 1`, code, ).Scan(&id); err != nil { return "", err } return id, nil } return "", err } return lotID, nil } func classifyLot(component HardwareComponent) (string, *string) { switch component.ComponentType { case "cpu": return classifyCpuLot(component) case "memory": return classifyMemoryLot(component) case "storage": return classifyStorageLot(component) case "psu": return classifyPsULot(component) case "pcie": return classifyPCIELot(component) default: return "", nil } } func classifyCpuLot(component HardwareComponent) (string, *string) { vendor := normalizeLotPointer(component.Vendor) model := normalizeLotPointer(component.Model) if vendor == "" && model == "" { return "", nil } parts := []string{"CPU"} if vendor != "" { parts = append(parts, vendor) } if model != "" { parts = append(parts, model) } code := strings.Join(parts, "_") var desc *string if component.Model != nil { desc = component.Model } else if component.Vendor != nil { desc = component.Vendor } return code, desc } func classifyMemoryLot(component HardwareComponent) (string, *string) { slotType := normalizeLotStringFromAttributes(component.Attributes, "type") sizeMB, ok := floatAttribute(component.Attributes, "size_mb") if !ok || sizeMB <= 0 { return "", nil } sizeGB := int(sizeMB / 1024) if sizeGB <= 0 { sizeGB = 1 } if slotType == "" { slotType = "UNKNOWN" } code := fmt.Sprintf("DIMM_%s_%dGB", slotType, sizeGB) return code, component.Model } func classifyStorageLot(component HardwareComponent) (string, *string) { driveType := normalizeLotStringFromAttributes(component.Attributes, "type") interfaceType := normalizeLotStringFromAttributes(component.Attributes, "interface") sizeGB, ok := floatAttribute(component.Attributes, "size_gb") if !ok || sizeGB <= 0 { return "", nil } sizeLabel := formatStorageSize(sizeGB) if driveType == "" { driveType = "STORAGE" } parts := []string{driveType} if interfaceType != "" { parts = append(parts, interfaceType) } parts = append(parts, sizeLabel) code := strings.Join(parts, "_") return code, component.Model } func classifyPsULot(component HardwareComponent) (string, *string) { wattage, ok := floatAttribute(component.Attributes, "wattage_w") if !ok || wattage <= 0 { return "", nil } vendor := normalizeLotPointer(component.Vendor) if vendor == "" { vendor = normalizeLotStringFromAttributes(component.Attributes, "vendor") } if vendor == "" { vendor = "UNKNOWN" } code := fmt.Sprintf("PSU_%dW_%s", int(wattage), vendor) return code, component.Model } func classifyPCIELot(component HardwareComponent) (string, *string) { deviceClass := normalizeLotStringFromAttributes(component.Attributes, "device_class") if deviceClass == "" { deviceClass = "PCIE" } model := normalizeLotPointer(component.Model) if model != "" { code := fmt.Sprintf("PCIE_%s_%s", deviceClass, model) return code, component.Model } vendorID, hasVendorID := intAttribute(component.Attributes, "vendor_id") deviceID, hasDeviceID := intAttribute(component.Attributes, "device_id") if hasVendorID && hasDeviceID { code := fmt.Sprintf("PCIE_%s_%d_%d", deviceClass, vendorID, deviceID) return code, component.Model } return "", nil } func normalizeLotPointer(value *string) string { if value == nil { return "" } return normalizeLotPart(*value) } func normalizeLotStringFromAttributes(attrs map[string]any, key string) string { if attrs == nil { return "" } if v, ok := attrs[key]; ok { switch typed := v.(type) { case string: return normalizeLotPart(typed) case float64: return normalizeLotPart(strconv.FormatFloat(typed, 'f', -1, 64)) case int: return normalizeLotPart(strconv.Itoa(typed)) case int64: return normalizeLotPart(strconv.FormatInt(int64(typed), 10)) } } return "" } func floatAttribute(attrs map[string]any, key string) (float64, bool) { if attrs == nil { return 0, false } if v, ok := attrs[key]; ok { switch typed := v.(type) { case float64: return typed, true case int: return float64(typed), true case int64: return float64(typed), true } } return 0, false } func intAttribute(attrs map[string]any, key string) (int, bool) { if attrs == nil { return 0, false } if v, ok := attrs[key]; ok { switch typed := v.(type) { case float64: return int(typed), true case int: return typed, true case int64: return int(typed), true } } return 0, false } func formatStorageSize(gb float64) string { if gb >= 1000 { tb := gb / 1000 if tb == float64(int64(tb)) { return fmt.Sprintf("%dTB", int64(tb)) } return fmt.Sprintf("%.2fTB", tb) } if gb == float64(int64(gb)) { return fmt.Sprintf("%dGB", int64(gb)) } return fmt.Sprintf("%.2fGB", gb) } func normalizeLotPart(value string) string { trimmed := strings.TrimSpace(value) if trimmed == "" { return "" } upper := strings.ToUpper(trimmed) cleaned := lotNormalizePattern.ReplaceAllString(upper, "_") cleaned = strings.Trim(cleaned, "_") cleaned = strings.ReplaceAll(cleaned, "__", "_") return cleaned }