package service import ( "context" "encoding/json" "errors" "fmt" "log/slog" "slices" "strings" "time" "github.com/AFASystems/presence/internal/pkg/kafkaclient" "github.com/AFASystems/presence/internal/pkg/model" "github.com/google/uuid" "github.com/segmentio/kafka-go" "gorm.io/gorm" ) func findTracker(msg model.HTTPLocation, db *gorm.DB) (model.Tracker, error) { var tracker model.Tracker if msg.MAC != "" { if err := db.Where("mac = ?", strings.ToUpper(strings.ReplaceAll(msg.MAC, ":", ""))).Find(&tracker).Error; err != nil { return model.Tracker{}, err } return tracker, nil } if msg.ID != "" { if err := db.Where("id = ?", msg.ID).Find(&tracker).Error; err != nil { return model.Tracker{}, err } return tracker, nil } return model.Tracker{}, errors.New("both ID and MAC are not provided") } func findZones(trackerID string, db *gorm.DB) ([]string, error) { var zones []model.TrackerZones if err := db.Select("zoneList").Where("tracker = ?", trackerID).Find(&zones).Error; err != nil { return nil, err } var allowedZones []string for _, z := range zones { allowedZones = append(allowedZones, z.ZoneList...) } return allowedZones, nil } func LocationToBeaconService(msg model.HTTPLocation, db *gorm.DB, writer *kafka.Writer, ctx context.Context) { tracker, err := findTracker(msg, db) if err != nil { msg := fmt.Sprintf("Error in finding tracker: %v", err) slog.Error(msg) return } allowedZones, err := findZones(tracker.ID, db) if err != nil { msg := fmt.Sprintf("Error in finding zones: %v", err) slog.Error(msg) return } var gw model.Gateway mac := formatMac(msg.Location) if err := db.Select("*").Where("mac = ?", mac).First(&gw).Error; err != nil { msg := fmt.Sprintf("Gateway not found for MAC: %s", mac) slog.Error(msg) return } var floor model.Floor if err := db.Where("id = ?", gw.Floor).First(&floor).Error; err != nil { msg := fmt.Sprintf("Floor not found for ID: %s", gw.Floor) slog.Error(msg) return } if err := db.Create(&model.Tracks{ UUID: msg.ID, Timestamp: time.Now(), Gateway: gw.ID, GatewayMac: gw.MAC, Tracker: msg.ID, Floor: gw.Floor, Building: gw.Building, TrackerMac: tracker.MAC, Signal: msg.RSSI, X: gw.X, Y: gw.Y, Z: float32(floor.FloorNumber), }).Error; err != nil { msg := fmt.Sprintf("Error in saving distance for beacon: %v", err) slog.Error(msg) return } err = db.Where("id = ?", msg.ID).Updates(model.Tracker{Position: gw.ID, X: gw.X, Y: gw.Y, Floor: floor.ID}).Error if err != nil { msg := fmt.Sprintf("Error in updating tracker: %v", err) slog.Error(msg) return } sendRestrictedZoneAlert(gw.ID, msg.ID, writer, ctx, allowedZones, db) } func LocationToBeaconServiceAI(msg model.HTTPLocation, db *gorm.DB, writer *kafka.Writer, ctx context.Context) { tracker, err := findTracker(msg, db) if err != nil { msg := fmt.Sprintf("Error in finding tracker: %v", err) slog.Error(msg) return } allowedZones, err := findZones(tracker.ID, db) if err != nil { msg := fmt.Sprintf("Error in finding zones: %v", err) slog.Error(msg) return } var gw model.Gateway if err := db.Order(fmt.Sprintf("POW(x - %f, 2) + POW(y - %f, 2)", msg.X, msg.Y)).First(&gw).Error; err != nil { msg := fmt.Sprintf("Error in finding gateway: %v", err) slog.Error(msg) return } if err := db.Create(&model.Tracks{UUID: tracker.ID, Timestamp: time.Now(), Gateway: gw.ID, GatewayMac: gw.MAC, Tracker: tracker.ID, Floor: gw.Floor, Building: gw.Building, TrackerMac: tracker.MAC, X: msg.X, Y: msg.Y, Z: msg.Z}).Error; err != nil { msg := fmt.Sprintf("Error in saving distance for beacon: %v", err) slog.Error(msg) return } var floor model.Floor floorFound := msg.Z >= 0 if floorFound { if err := db.Where("floor_number = ?", int(msg.Z)).First(&floor).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { slog.Warn(fmt.Sprintf("Floor not found for floor number: %f, skipping floor update", msg.Z)) floorFound = false } else { slog.Error(fmt.Sprintf("Error querying floor for floor number: %f: %v", msg.Z, err)) return } } } var trackerUpdate model.Tracker if floorFound { trackerUpdate = model.Tracker{Position: gw.ID, X: msg.X, Y: msg.Y, Floor: floor.ID} } else { trackerUpdate = model.Tracker{Position: gw.ID, X: msg.X, Y: msg.Y} } err = db.Where("id = ?", tracker.ID).Updates(trackerUpdate).Error if err != nil { slog.Error(fmt.Sprintf("Error in updating tracker: %v", err)) return } sendRestrictedZoneAlert(gw.ID, tracker.ID, writer, ctx, allowedZones, db) } func SendAlert(trackerId, alertType string, writer *kafka.Writer, ctx context.Context, db *gorm.DB) { var existingAlert model.Alert result := db.Select("status").Where("tracker_id = ? AND type = ?", trackerId, alertType).Order("timestamp DESC").Limit(1).Find(&existingAlert) if result.RowsAffected == 0 || existingAlert.Status == "resolved" { alert := model.Alert{ ID: uuid.New().String(), TrackerID: trackerId, Type: alertType, Status: "new", Timestamp: time.Now(), } if err := InsertAlert(alert, db, ctx); err != nil { msg := fmt.Sprintf("Error in inserting alert: %v", err) slog.Error(msg) return } eMsg, err := json.Marshal(alert) if err != nil { msg := "Error in marshaling" slog.Error(msg) return } else { msg := kafka.Message{ Value: eMsg, } if err := kafkaclient.Write(ctx, writer, msg); err != nil { msg := fmt.Sprintf("Error in writing message: %v", err) slog.Error(msg) return } } return } else { return } } func sendRestrictedZoneAlert(gwId, trackerId string, writer *kafka.Writer, ctx context.Context, allowedZones []string, db *gorm.DB) { if len(allowedZones) != 0 && !slices.Contains(allowedZones, gwId) { SendAlert(trackerId, "Restricted zone", writer, ctx, db) } } func formatMac(MAC string) string { var res strings.Builder for i := 0; i < len(MAC); i += 2 { if i > 0 { res.WriteByte(':') } end := min(i+2, len(MAC)) res.WriteString(MAC[i:end]) } return res.String() }