package localstore import ( "errors" "strings" "sync" "time" "unicode/utf8" "airlines/pkg/model" ) type userNameKey struct { Surname string Name string Fathersname string // "" allowed BirthYMD int32 // 0 if unknown } type userInitKey struct { Surname string Name string Init string // one letter BirthYMD int32 } type cardKey struct { Prefix string Bonus string // may be "" Number uint64 } type cardPairKey struct { Prefix string Number uint64 } type flightKey struct { Number string From string To string DateYMD int32 HasTime bool Sec int32 // seconds since local midnight if HasTime } type flightDayKey struct { Number string From string To string DateYMD int32 } type Store struct { mu sync.RWMutex users []*model.User cards []*model.Card flights []*model.Flight nickToUID map[string]uint64 nameToUID map[userNameKey]uint64 nameInitToUID map[userInitKey]uint64 cardToCID map[cardKey]uint64 cardPairToCID map[cardPairKey]uint64 flightToFID map[flightKey]uint64 flightByDay map[flightDayKey]uint64 userFlights map[uint64]map[uint64]struct{} // user_id -> set(flight_id) cardFlights map[uint64]map[uint64]struct{} // card_id -> set(flight_id) codesByUser map[uint64]map[string]struct{} // user_id -> set(code) countriesByUser map[uint64]map[string]struct{} // user_id -> set(country) cardsByUser map[uint64]map[uint64]struct{} // user_id -> set(card_id) } func NewLocalStore() *Store { return &Store{ users: make([]*model.User, 1), cards: make([]*model.Card, 1), flights: make([]*model.Flight, 1), nickToUID: make(map[string]uint64, 1<<12), nameToUID: make(map[userNameKey]uint64, 1<<12), cardToCID: make(map[cardKey]uint64, 1<<14), cardPairToCID: make(map[cardPairKey]uint64, 1<<14), flightToFID: make(map[flightKey]uint64, 1<<15), flightByDay: make(map[flightDayKey]uint64, 1<<15), userFlights: make(map[uint64]map[uint64]struct{}, 1<<12), cardFlights: make(map[uint64]map[uint64]struct{}, 1<<12), nameInitToUID: make(map[userInitKey]uint64, 1<<12), codesByUser: make(map[uint64]map[string]struct{}, 1<<12), countriesByUser: make(map[uint64]map[string]struct{}, 1<<12), cardsByUser: make(map[uint64]map[uint64]struct{}, 1<<12), } } func isZeroCoord(c model.LatLong) bool { return c.Lat == 0 && c.Long == 0 } func ymdUTC(t time.Time) int32 { if t.IsZero() || t.Year() == model.SentinelBirthday().Year() { return 0 } u := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC) return int32(u.Year()*10000 + int(u.Month())*100 + u.Day()) } func secSinceMidnight(t time.Time) int32 { h, m, s := t.Clock() return int32(h*3600 + m*60 + s) } func ensureSet(m map[uint64]map[uint64]struct{}, k uint64) map[uint64]struct{} { s, ok := m[k] if !ok { s = make(map[uint64]struct{}, 8) m[k] = s } return s } /* ============================== users ============================= */ func fatherInitial(s string) string { s = strings.TrimSpace(s) if s == "" { return "" } r, _ := utf8.DecodeRuneInString(s) if r == utf8.RuneError { return "" } return string(r) // your pipeline keeps it UPPER } func addUserInitIndex(m map[userInitKey]uint64, u *model.User) { k := userInitKey{Surname: u.Surname, Name: u.Name, Init: fatherInitial(u.Fathersname), BirthYMD: ymdUTC(u.Birthday)} if _, exists := m[k]; !exists { m[k] = u.ID } } func delUserInitIndex(m map[userInitKey]uint64, u *model.User) { k := userInitKey{Surname: u.Surname, Name: u.Name, Init: fatherInitial(u.Fathersname), BirthYMD: ymdUTC(u.Birthday)} delete(m, k) } // --- merge helper (unchanged; keeps initial→full, birthday, nick, sex upgrades) --- func (s *Store) mergeUserFields(id uint64, in *model.User) *model.User { ex := s.users[id] // fathersname: initial -> full (same initial), move indexes if ex.Fathersname != "" && len([]rune(ex.Fathersname)) == 1 && in.Fathersname != "" && fatherInitial(ex.Fathersname) == fatherInitial(in.Fathersname) && ex.Fathersname != in.Fathersname { oldNameKey := userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)} if _, ok := s.nameToUID[oldNameKey]; ok { delete(s.nameToUID, oldNameKey) } delUserInitIndex(s.nameInitToUID, ex) ex.Fathersname = in.Fathersname s.nameToUID[userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)}] = id addUserInitIndex(s.nameInitToUID, ex) } // birthday upgrade if ymdUTC(ex.Birthday) == 0 && ymdUTC(in.Birthday) != 0 { oldNameKey := userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: 0} if _, ok := s.nameToUID[oldNameKey]; ok { delete(s.nameToUID, oldNameKey) } delUserInitIndex(s.nameInitToUID, ex) ex.Birthday = in.Birthday s.nameToUID[userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)}] = id addUserInitIndex(s.nameInitToUID, ex) } // nick/sex if ex.Nick == "" && in.Nick != "" { ex.Nick = in.Nick s.nickToUID[in.Nick] = id } if ex.Sex == model.SexUnknown && in.Sex != model.SexUnknown { ex.Sex = in.Sex } return ex } // --- FIXED SaveUser: initial-key lookup tries BOTH (birth, 0) --- func (s *Store) SaveUser(u *model.User) (*model.User, error) { if u == nil { return nil, errors.New("nil user") } // normalize (names already UPPER) u.Nick = strings.TrimSpace(u.Nick) u.Name = strings.TrimSpace(u.Name) u.Surname = strings.TrimSpace(u.Surname) u.Fathersname = strings.Trim(strings.TrimSpace(u.Fathersname), ".,") if u.Birthday.IsZero() { u.Birthday = model.SentinelBirthday() } inBirth := ymdUTC(u.Birthday) inKey := userNameKey{Surname: u.Surname, Name: u.Name, Fathersname: u.Fathersname, BirthYMD: inBirth} s.mu.Lock() defer s.mu.Unlock() // 1) by Nick if u.Nick != "" { if id, ok := s.nickToUID[u.Nick]; ok { return s.mergeUserFields(id, u), nil } } // 2) exact tuple if id, ok := s.nameToUID[inKey]; ok { if u.Nick != "" && s.users[id].Nick == "" { s.users[id].Nick = u.Nick s.nickToUID[u.Nick] = id } return s.mergeUserFields(id, u), nil } // 3) initial-based match (try with incoming birth, then with 0) init := fatherInitial(u.Fathersname) tryInits := []userInitKey{ {Surname: u.Surname, Name: u.Name, Init: init, BirthYMD: inBirth}, } if inBirth != 0 { tryInits = append(tryInits, userInitKey{Surname: u.Surname, Name: u.Name, Init: init, BirthYMD: 0}) } for _, ik := range tryInits { if id, ok := s.nameInitToUID[ik]; ok { ex := s.users[id] // If ex has initial-only and incoming has full (same initial) → upgrade fathers + move indexes if ex.Fathersname == fatherInitial(ex.Fathersname) && u.Fathersname != "" && fatherInitial(u.Fathersname) == fatherInitial(ex.Fathersname) { // move name index oldNameKey := userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)} delete(s.nameToUID, oldNameKey) // remove old init index (birth may differ) delUserInitIndex(s.nameInitToUID, ex) ex.Fathersname = u.Fathersname newNameKey := userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)} s.nameToUID[newNameKey] = id addUserInitIndex(s.nameInitToUID, ex) } // Upgrade birthday if needed if ymdUTC(ex.Birthday) == 0 && inBirth != 0 { // move name key 0 -> inBirth oldNameKey := userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: 0} if _, ok2 := s.nameToUID[oldNameKey]; ok2 { delete(s.nameToUID, oldNameKey) } // move init index 0 -> inBirth delUserInitIndex(s.nameInitToUID, ex) ex.Birthday = u.Birthday s.nameToUID[userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: inBirth}] = id addUserInitIndex(s.nameInitToUID, ex) } // nick/sex upgrades if u.Nick != "" && ex.Nick == "" { ex.Nick = u.Nick s.nickToUID[u.Nick] = id } if ex.Sex == model.SexUnknown && u.Sex != model.SexUnknown { ex.Sex = u.Sex } return ex, nil } } // 4) relaxed: fathersname empty, same birth if id, ok := s.nameToUID[userNameKey{Surname: u.Surname, Name: u.Name, Fathersname: "", BirthYMD: inBirth}]; ok { ex := s.users[id] if ex.Fathersname == "" && u.Fathersname != "" { delete(s.nameToUID, userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: "", BirthYMD: inBirth}) ex.Fathersname = u.Fathersname s.nameToUID[userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: inBirth}] = id addUserInitIndex(s.nameInitToUID, ex) } if u.Nick != "" && ex.Nick == "" { ex.Nick = u.Nick s.nickToUID[u.Nick] = id } if ex.Sex == model.SexUnknown && u.Sex != model.SexUnknown { ex.Sex = u.Sex } if ymdUTC(ex.Birthday) == 0 && inBirth != 0 { ex.Birthday = u.Birthday } return ex, nil } // 5) same fathersname, no birth if id, ok := s.nameToUID[userNameKey{Surname: u.Surname, Name: u.Name, Fathersname: u.Fathersname, BirthYMD: 0}]; ok { delete(s.nameToUID, userNameKey{Surname: u.Surname, Name: u.Name, Fathersname: u.Fathersname, BirthYMD: 0}) ex := s.users[id] ex.Birthday = u.Birthday s.nameToUID[inKey] = id delUserInitIndex(s.nameInitToUID, ex) addUserInitIndex(s.nameInitToUID, ex) if u.Nick != "" && ex.Nick == "" { ex.Nick = u.Nick s.nickToUID[u.Nick] = id } if ex.Sex == model.SexUnknown && u.Sex != model.SexUnknown { ex.Sex = u.Sex } return ex, nil } // 6) fully unspecific existing (fathers="", birth=0) if id, ok := s.nameToUID[userNameKey{Surname: u.Surname, Name: u.Name, Fathersname: "", BirthYMD: 0}]; ok { ex := s.users[id] delete(s.nameToUID, userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: "", BirthYMD: 0}) if ex.Fathersname == "" && u.Fathersname != "" { ex.Fathersname = u.Fathersname } if ymdUTC(ex.Birthday) == 0 && inBirth != 0 { ex.Birthday = u.Birthday } s.nameToUID[userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)}] = id addUserInitIndex(s.nameInitToUID, ex) if u.Nick != "" && ex.Nick == "" { ex.Nick = u.Nick s.nickToUID[u.Nick] = id } if ex.Sex == model.SexUnknown && u.Sex != model.SexUnknown { ex.Sex = u.Sex } return ex, nil } // 7) create u.ID = uint64(len(s.users)) s.users = append(s.users, u) if u.Nick != "" { s.nickToUID[u.Nick] = u.ID } s.nameToUID[inKey] = u.ID addUserInitIndex(s.nameInitToUID, u) return u, nil } /* ============================== cards ============================= */ /* Match order: 1) exact (Prefix, Number, Bonus) 2) pair (Prefix, Number) → if stored bonus=="" and incoming bonus!="", upgrade in place (move triple index) 3) else create new Never steal UserID: only set if existing has 0 and incoming non-zero. */ func (s *Store) SaveCard(c *model.Card) (*model.Card, error) { if c == nil { return nil, errors.New("nil card") } c.Prefix = strings.TrimSpace(c.Prefix) c.Bonusprogramm = strings.TrimSpace(c.Bonusprogramm) if c.Prefix == "" { return nil, errors.New("invalid card: empty prefix") } tri := cardKey{Prefix: c.Prefix, Number: c.Number, Bonus: c.Bonusprogramm} pair := cardPairKey{Prefix: c.Prefix, Number: c.Number} s.mu.Lock() defer s.mu.Unlock() // exact triple if id, ok := s.cardToCID[tri]; ok { ex := s.cards[id] if ex.UserID == 0 && c.UserID != 0 { ex.UserID = c.UserID if s.cardsByUser[ex.UserID] == nil { s.cardsByUser[ex.UserID] = make(map[uint64]struct{}, 1024) } v := s.cardsByUser[ex.UserID] v[ex.ID] = struct{}{} s.cardsByUser[ex.UserID] = v } return ex, nil } // by pair if id, ok := s.cardPairToCID[pair]; ok { ex := s.cards[id] // link user once if ex.UserID == 0 && c.UserID != 0 { ex.UserID = c.UserID } if s.cardsByUser[ex.UserID] == nil { s.cardsByUser[ex.UserID] = make(map[uint64]struct{}, 1024) } v := s.cardsByUser[ex.UserID] v[ex.ID] = struct{}{} s.cardsByUser[ex.UserID] = v switch { case ex.Bonusprogramm == "" && c.Bonusprogramm != "": // move triple index from empty -> new bonus oldTri := cardKey{Prefix: ex.Prefix, Number: ex.Number, Bonus: ex.Bonusprogramm} delete(s.cardToCID, oldTri) ex.Bonusprogramm = c.Bonusprogramm newTri := cardKey{Prefix: ex.Prefix, Number: ex.Number, Bonus: ex.Bonusprogramm} s.cardToCID[newTri] = id return ex, nil case ex.Bonusprogramm == "" && c.Bonusprogramm == "": return ex, nil case ex.Bonusprogramm != "" && c.Bonusprogramm == "": return ex, nil case ex.Bonusprogramm != "" && c.Bonusprogramm != "" && ex.Bonusprogramm != c.Bonusprogramm: // different program → create new card record default: return ex, nil } } // create c.ID = uint64(len(s.cards)) s.cards = append(s.cards, c) s.cardPairToCID[pair] = c.ID s.cardToCID[tri] = c.ID // even if bonus == "", we still index triple if s.cardsByUser[c.UserID] == nil { s.cardsByUser[c.UserID] = make(map[uint64]struct{}, 1024) } v := s.cardsByUser[c.UserID] v[c.ID] = struct{}{} s.cardsByUser[c.UserID] = v return c, nil } /* ============================== flights =========================== */ /* Identity: - date-only: (Number, From, To, DateYMD, false, 0) - timed : (Number, From, To, DateYMD, true, SecSinceMidnight) Upgrade: - if a date-only exists and a timed arrives for the same day, upgrade in place Merge: - coords: fill when missing - relations: add (dedup via sets) */ func (s *Store) SaveFlight(f *model.Flight) (*model.Flight, error) { if f == nil { return nil, errors.New("nil flight") } f.Number = strings.TrimSpace(f.Number) f.From = strings.TrimSpace(f.From) f.To = strings.TrimSpace(f.To) // normalize day dayUTC := time.Date(f.Date.Year(), f.Date.Month(), f.Date.Day(), 0, 0, 0, 0, time.UTC) ymd := ymdUTC(dayUTC) var pKey flightKey if f.HasTime { pKey = flightKey{Number: f.Number, From: f.From, To: f.To, DateYMD: ymd, HasTime: true, Sec: secSinceMidnight(f.Date)} } else { f.Date = dayUTC // store as date-only pKey = flightKey{Number: f.Number, From: f.From, To: f.To, DateYMD: ymd, HasTime: false, Sec: 0} } dayKey := flightDayKey{Number: f.Number, From: f.From, To: f.To, DateYMD: ymd} s.mu.Lock() defer s.mu.Unlock() // 1) exact (precise) key if id, ok := s.flightToFID[pKey]; ok { ex := s.flights[id] s.mergeFlightFields(id, ex, f) return ex, nil } // 2) same day exists -> maybe upgrade date-only to timed if id, ok := s.flightByDay[dayKey]; ok { ex := s.flights[id] exKey := s.keyOfFlight(ex) if !ex.HasTime && f.HasTime { // move map key to timed delete(s.flightToFID, exKey) ex.HasTime = true // set clock from incoming (keep same calendar date) ex.Date = time.Date(dayUTC.Year(), dayUTC.Month(), dayUTC.Day(), f.Date.Hour(), f.Date.Minute(), f.Date.Second(), f.Date.Nanosecond(), f.Date.Location()) s.flightToFID[s.keyOfFlight(ex)] = id // day index already points to best precision } // merge fields/relations s.mergeFlightFields(id, ex, f) return ex, nil } // 3) brand new f.ID = uint64(len(s.flights)) s.flights = append(s.flights, f) s.flightToFID[pKey] = f.ID s.flightByDay[dayKey] = f.ID // if s.countriesByUser[f.UserID] == nil { // s.countriesByUser[f.UserID] = make(map[string]struct{}, 1024) // } // v := s.countriesByUser[f.UserID] // dd, _ := airports.LookupIATA(f.From) // v[dd.Country] = struct{}{} // s.countriesByUser[f.UserID] = v if f.Code != "" { if s.codesByUser[f.UserID] == nil { s.codesByUser[f.UserID] = make(map[string]struct{}, 1024) } codesByUser := s.codesByUser[f.UserID] codesByUser[f.Code] = struct{}{} s.codesByUser[f.UserID] = codesByUser } // relations if f.UserID != 0 { ensureSet(s.userFlights, f.UserID)[f.ID] = struct{}{} } if f.CardID != 0 { ensureSet(s.cardFlights, f.CardID)[f.ID] = struct{}{} } return f, nil } func (s *Store) keyOfFlight(f *model.Flight) flightKey { ymd := ymdUTC(time.Date(f.Date.Year(), f.Date.Month(), f.Date.Day(), 0, 0, 0, 0, time.UTC)) if f.HasTime { return flightKey{Number: f.Number, From: f.From, To: f.To, DateYMD: ymd, HasTime: true, Sec: secSinceMidnight(f.Date)} } return flightKey{Number: f.Number, From: f.From, To: f.To, DateYMD: ymd, HasTime: false, Sec: 0} } func (s *Store) mergeFlightFields(id uint64, ex, in *model.Flight) { // coords: fill when empty if isZeroCoord(ex.FromCoords) && !isZeroCoord(in.FromCoords) { ex.FromCoords = in.FromCoords } if isZeroCoord(ex.ToCoords) && !isZeroCoord(in.ToCoords) { ex.ToCoords = in.ToCoords } // relations if in.UserID != 0 { ensureSet(s.userFlights, in.UserID)[id] = struct{}{} } if in.CardID != 0 { ensureSet(s.cardFlights, in.CardID)[id] = struct{}{} } if in.Code != "" && ex.Code == "" { ex.Code = in.Code // if s.codesByUser[in.UserID] == nil { // s.codesByUser[in.UserID] = make(map[string]struct{}, 1024) // } // codesByUser := s.codesByUser[in.UserID] // codesByUser[in.Code] = struct{}{} // s.codesByUser[in.UserID] = codesByUser } } /* ============================== finders =========================== */ func (s *Store) FindUserByNick(nick string) (*model.User, bool) { s.mu.RLock() defer s.mu.RUnlock() id, ok := s.nickToUID[strings.TrimSpace(nick)] if !ok || id == 0 || int(id) >= len(s.users) { return nil, false } return s.users[id], true } func (s *Store) FindUserByName(name, surname, fathers string, bday time.Time) (*model.User, bool) { key := userNameKey{ Surname: strings.TrimSpace(surname), Name: strings.TrimSpace(name), Fathersname: strings.TrimSpace(fathers), BirthYMD: ymdUTC(bday), } s.mu.RLock() defer s.mu.RUnlock() id, ok := s.nameToUID[key] if !ok || id == 0 || int(id) >= len(s.users) { return nil, false } return s.users[id], true } func (s *Store) FindCard(prefix string, number uint64, bonus string) (*model.Card, bool) { tri := cardKey{Prefix: strings.TrimSpace(prefix), Number: number, Bonus: strings.TrimSpace(bonus)} s.mu.RLock() defer s.mu.RUnlock() if id, ok := s.cardToCID[tri]; ok && id != 0 && int(id) < len(s.cards) { return s.cards[id], true } // fall back to pair if no exact pair := cardPairKey{Prefix: strings.TrimSpace(prefix), Number: number} if id, ok := s.cardPairToCID[pair]; ok && id != 0 && int(id) < len(s.cards) { return s.cards[id], true } return nil, false } func (s *Store) FindFlight(number, from, to string, date time.Time, hasTime bool) (*model.Flight, bool) { number = strings.TrimSpace(number) from = strings.TrimSpace(from) to = strings.TrimSpace(to) ymd := ymdUTC(time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC)) var k flightKey if hasTime { k = flightKey{Number: number, From: from, To: to, DateYMD: ymd, HasTime: true, Sec: secSinceMidnight(date)} } else { k = flightKey{Number: number, From: from, To: to, DateYMD: ymd, HasTime: false, Sec: 0} } s.mu.RLock() defer s.mu.RUnlock() if id, ok := s.flightToFID[k]; ok && id != 0 && int(id) < len(s.flights) { return s.flights[id], true } // day-level fallback (returns best precision for the day if exact key absent) if id, ok := s.flightByDay[flightDayKey{Number: number, From: from, To: to, DateYMD: ymd}]; ok && id != 0 && int(id) < len(s.flights) { return s.flights[id], true } return nil, false }