package database import ( "database/sql" "fmt" "regexp" "strings" "sync" "time" ) type CallsignManager struct { db *sql.DB mutex sync.RWMutex // Compiled regex patterns for callsign parsing airlinePattern *regexp.Regexp flightPattern *regexp.Regexp } type CallsignParseResult struct { OriginalCallsign string AirlineCode string FlightNumber string IsValid bool ParsedTime time.Time } func NewCallsignManager(db *sql.DB) *CallsignManager { return &CallsignManager{ db: db, // Match airline code (2-3 letters) followed by flight number (1-4 digits, optional letter) airlinePattern: regexp.MustCompile(`^([A-Z]{2,3})([0-9]{1,4}[A-Z]?)$`), // More flexible pattern for general flight identification flightPattern: regexp.MustCompile(`^([A-Z0-9]+)([0-9]+[A-Z]?)$`), } } func (cm *CallsignManager) ParseCallsign(callsign string) *CallsignParseResult { result := &CallsignParseResult{ OriginalCallsign: callsign, ParsedTime: time.Now(), IsValid: false, } if callsign == "" { return result } // Clean and normalize the callsign normalized := strings.TrimSpace(strings.ToUpper(callsign)) // Try airline pattern first (most common for commercial flights) if matches := cm.airlinePattern.FindStringSubmatch(normalized); len(matches) == 3 { result.AirlineCode = matches[1] result.FlightNumber = matches[2] result.IsValid = true return result } // Fall back to general flight pattern if matches := cm.flightPattern.FindStringSubmatch(normalized); len(matches) == 3 { result.AirlineCode = matches[1] result.FlightNumber = matches[2] result.IsValid = true return result } return result } func (cm *CallsignManager) GetCallsignInfo(callsign string) (*CallsignInfo, error) { cm.mutex.RLock() defer cm.mutex.RUnlock() if callsign == "" { return nil, fmt.Errorf("empty callsign") } // First check the cache cached, err := cm.getCallsignFromCache(callsign) if err == nil && cached != nil { return cached, nil } // Parse the callsign parsed := cm.ParseCallsign(callsign) if !parsed.IsValid { return &CallsignInfo{ OriginalCallsign: callsign, IsValid: false, }, nil } // Look up airline information airline, err := cm.getAirlineByCode(parsed.AirlineCode) if err != nil && err != sql.ErrNoRows { return nil, fmt.Errorf("failed to lookup airline %s: %w", parsed.AirlineCode, err) } // Build the result info := &CallsignInfo{ OriginalCallsign: callsign, AirlineCode: parsed.AirlineCode, FlightNumber: parsed.FlightNumber, IsValid: true, LastUpdated: time.Now(), } if airline != nil { info.AirlineName = airline.Name info.AirlineCountry = airline.Country info.DisplayName = fmt.Sprintf("%s Flight %s", airline.Name, parsed.FlightNumber) } else { info.DisplayName = fmt.Sprintf("%s %s", parsed.AirlineCode, parsed.FlightNumber) } // Cache the result (fire and forget) go func() { if err := cm.cacheCallsignInfo(info); err != nil { // Log error but don't fail the lookup fmt.Printf("Warning: failed to cache callsign info for %s: %v\n", callsign, err) } }() return info, nil } func (cm *CallsignManager) getCallsignFromCache(callsign string) (*CallsignInfo, error) { query := ` SELECT callsign, airline_icao, flight_number, airline_name, airline_country, '', 1, cached_at, expires_at FROM callsign_cache WHERE callsign = ? AND expires_at > datetime('now') ` var info CallsignInfo var cacheExpires time.Time err := cm.db.QueryRow(query, callsign).Scan( &info.OriginalCallsign, &info.AirlineCode, &info.FlightNumber, &info.AirlineName, &info.AirlineCountry, &info.DisplayName, &info.IsValid, &info.LastUpdated, &cacheExpires, ) if err != nil { return nil, err } return &info, nil } func (cm *CallsignManager) cacheCallsignInfo(info *CallsignInfo) error { // Cache for 24 hours by default cacheExpires := time.Now().Add(24 * time.Hour) query := ` INSERT OR REPLACE INTO callsign_cache (callsign, airline_icao, flight_number, airline_name, airline_country, cached_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?) ` _, err := cm.db.Exec(query, info.OriginalCallsign, info.AirlineCode, info.FlightNumber, info.AirlineName, info.AirlineCountry, info.LastUpdated, cacheExpires, ) return err } func (cm *CallsignManager) getAirlineByCode(code string) (*AirlineRecord, error) { query := ` SELECT icao_code, iata_code, name, country, active FROM airlines WHERE (icao_code = ? OR iata_code = ?) AND active = 1 ORDER BY CASE WHEN icao_code = ? THEN 1 ELSE 2 END, name LIMIT 1 ` var airline AirlineRecord err := cm.db.QueryRow(query, code, code, code).Scan( &airline.ICAOCode, &airline.IATACode, &airline.Name, &airline.Country, &airline.Active, ) if err != nil { return nil, err } return &airline, nil } func (cm *CallsignManager) GetAirlinesByCountry(country string) ([]AirlineRecord, error) { cm.mutex.RLock() defer cm.mutex.RUnlock() query := ` SELECT icao_code, iata_code, name, country, active FROM airlines WHERE country = ? AND active = 1 ORDER BY name ` rows, err := cm.db.Query(query, country) if err != nil { return nil, err } defer rows.Close() var airlines []AirlineRecord for rows.Next() { var airline AirlineRecord err := rows.Scan( &airline.ICAOCode, &airline.IATACode, &airline.Name, &airline.Country, &airline.Active, ) if err != nil { return nil, err } airlines = append(airlines, airline) } return airlines, rows.Err() } func (cm *CallsignManager) SearchAirlines(query string) ([]AirlineRecord, error) { cm.mutex.RLock() defer cm.mutex.RUnlock() searchQuery := ` SELECT icao_code, iata_code, name, country, active FROM airlines WHERE ( name LIKE ? OR icao_code LIKE ? OR iata_code LIKE ? OR country LIKE ? ) AND active = 1 ORDER BY CASE WHEN name LIKE ? THEN 1 WHEN icao_code = ? OR iata_code = ? THEN 2 ELSE 3 END, name LIMIT 50 ` searchTerm := "%" + strings.ToUpper(query) + "%" exactTerm := strings.ToUpper(query) rows, err := cm.db.Query(searchQuery, searchTerm, searchTerm, searchTerm, searchTerm, exactTerm, exactTerm, exactTerm, ) if err != nil { return nil, err } defer rows.Close() var airlines []AirlineRecord for rows.Next() { var airline AirlineRecord err := rows.Scan( &airline.ICAOCode, &airline.IATACode, &airline.Name, &airline.Country, &airline.Active, ) if err != nil { return nil, err } airlines = append(airlines, airline) } return airlines, rows.Err() } func (cm *CallsignManager) ClearExpiredCache() error { cm.mutex.Lock() defer cm.mutex.Unlock() query := `DELETE FROM callsign_cache WHERE expires_at <= datetime('now')` _, err := cm.db.Exec(query) return err } func (cm *CallsignManager) GetCacheStats() (map[string]interface{}, error) { cm.mutex.RLock() defer cm.mutex.RUnlock() stats := make(map[string]interface{}) // Total cached entries var totalCached int err := cm.db.QueryRow(`SELECT COUNT(*) FROM callsign_cache`).Scan(&totalCached) if err != nil { return nil, err } stats["total_cached"] = totalCached // Valid (non-expired) entries var validCached int err = cm.db.QueryRow(`SELECT COUNT(*) FROM callsign_cache WHERE expires_at > datetime('now')`).Scan(&validCached) if err != nil { return nil, err } stats["valid_cached"] = validCached // Expired entries stats["expired_cached"] = totalCached - validCached // Total airlines in database var totalAirlines int err = cm.db.QueryRow(`SELECT COUNT(*) FROM airlines WHERE active = 1`).Scan(&totalAirlines) if err != nil { return nil, err } stats["total_airlines"] = totalAirlines return stats, nil } func (cm *CallsignManager) LoadEmbeddedData() error { // Check if airlines table has data var count int err := cm.db.QueryRow(`SELECT COUNT(*) FROM airlines`).Scan(&count) if err != nil { return err } if count > 0 { // Data already loaded return nil } // For now, we'll implement this as a placeholder // In a full implementation, this would load embedded airline data // from embedded files or resources return nil }