package rules import ( "database/sql" "fmt" "log" "strconv" "eq2emu/internal/database" ) // DatabaseService handles rule database operations // Converted from C++ WorldDatabase rule functions type DatabaseService struct { db *database.Database } // NewDatabaseService creates a new database service instance func NewDatabaseService(db *database.Database) *DatabaseService { return &DatabaseService{ db: db, } } // LoadGlobalRuleSet loads the global rule set from database // Converted from C++ WorldDatabase::LoadGlobalRuleSet() func (ds *DatabaseService) LoadGlobalRuleSet(ruleManager *RuleManager) error { if ds.db == nil { return fmt.Errorf("database not initialized") } ruleSetID := int32(0) // Get the default ruleset ID from variables table query := "SELECT variable_value FROM variables WHERE variable_name = ?" var variableValue string err := ds.db.QueryRow(query, DefaultRuleSetIDVar).Scan(&variableValue) if err != nil { if err == sql.ErrNoRows { log.Printf("[Rules] Variables table is missing %s variable name, using code-default rules", DefaultRuleSetIDVar) return nil } return fmt.Errorf("error querying default ruleset ID: %v", err) } if id, err := strconv.ParseInt(variableValue, 10, 32); err == nil { ruleSetID = int32(id) log.Printf("[Rules] Loading Global Ruleset id %d", ruleSetID) } else { return fmt.Errorf("invalid ruleset ID format: %s", variableValue) } if ruleSetID > 0 { if !ruleManager.SetGlobalRuleSet(ruleSetID) { return fmt.Errorf("error loading global rule set - rule set with ID %d does not exist", ruleSetID) } } ruleManager.stats.IncrementDatabaseOperations() return nil } // LoadRuleSets loads all rule sets from database // Converted from C++ WorldDatabase::LoadRuleSets() func (ds *DatabaseService) LoadRuleSets(ruleManager *RuleManager, reload bool) error { if ds.db == nil { return fmt.Errorf("database not initialized") } if reload { ruleManager.Flush(true) } // First load the coded defaults into the global rule set ruleManager.LoadCodedDefaultsIntoRuleSet(ruleManager.GetGlobalRuleSet()) // Load active rule sets from database query := "SELECT ruleset_id, ruleset_name FROM rulesets WHERE ruleset_active > 0" loadedCount := 0 rows, err := ds.db.Query(query) if err != nil { return fmt.Errorf("error querying rule sets: %v", err) } defer rows.Close() for rows.Next() { var ruleSetID int32 var ruleSetName string err := rows.Scan(&ruleSetID, &ruleSetName) if err != nil { return fmt.Errorf("error scanning rule set row: %v", err) } ruleSet := NewRuleSet() ruleSet.SetID(ruleSetID) ruleSet.SetName(ruleSetName) if ruleManager.AddRuleSet(ruleSet) { log.Printf("[Rules] Loading rule set '%s' (%d)", ruleSet.GetName(), ruleSet.GetID()) err := ds.LoadRuleSetDetails(ruleManager, ruleSet) if err != nil { log.Printf("[Rules] Error loading rule set details for '%s': %v", ruleSetName, err) continue // Continue with other rule sets } loadedCount++ } else { log.Printf("[Rules] Unable to add rule set '%s' - ID %d already exists", ruleSetName, ruleSetID) } } if err = rows.Err(); err != nil { return fmt.Errorf("error iterating rule sets: %v", err) } log.Printf("[Rules] Loaded %d Rule Sets", loadedCount) // Load global rule set err = ds.LoadGlobalRuleSet(ruleManager) if err != nil { return fmt.Errorf("error loading global rule set: %v", err) } ruleManager.stats.IncrementDatabaseOperations() return nil } // LoadRuleSetDetails loads the detailed rules for a specific rule set // Converted from C++ WorldDatabase::LoadRuleSetDetails() func (ds *DatabaseService) LoadRuleSetDetails(ruleManager *RuleManager, ruleSet *RuleSet) error { if ds.db == nil { return fmt.Errorf("database not initialized") } if ruleSet == nil { return fmt.Errorf("rule set is nil") } // Copy rules from global rule set (coded defaults) first ruleSet.CopyRulesInto(ruleManager.GetGlobalRuleSet()) // Load rule overrides from database query := "SELECT rule_category, rule_type, rule_value FROM ruleset_details WHERE ruleset_id = ?" loadedRules := 0 rows, err := ds.db.Query(query, ruleSet.GetID()) if err != nil { return fmt.Errorf("error querying rule set details: %v", err) } defer rows.Close() for rows.Next() { var categoryName, typeName, ruleValue string err := rows.Scan(&categoryName, &typeName, &ruleValue) if err != nil { return fmt.Errorf("error scanning rule detail row: %v", err) } // Find the rule by name rule := ruleSet.GetRuleByName(categoryName, typeName) if rule == nil { log.Printf("[Rules] Unknown rule with category '%s' and type '%s'", categoryName, typeName) continue // Continue with other rules } log.Printf("[Rules] Setting rule category '%s', type '%s' to value: %s", categoryName, typeName, ruleValue) rule.SetValue(ruleValue) loadedRules++ } if err = rows.Err(); err != nil { return fmt.Errorf("error iterating rule set details: %v", err) } log.Printf("[Rules] Loaded %d rule overrides for rule set '%s'", loadedRules, ruleSet.GetName()) ruleManager.stats.IncrementDatabaseOperations() return nil } // SaveRuleSet saves a rule set to the database func (ds *DatabaseService) SaveRuleSet(ruleSet *RuleSet) error { if ds.db == nil { return fmt.Errorf("database not initialized") } if ruleSet == nil { return fmt.Errorf("rule set is nil") } // Use transaction for atomicity tx, err := ds.db.Begin() if err != nil { return fmt.Errorf("error beginning transaction: %v", err) } defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }() // Insert or update rule set using MySQL ON DUPLICATE KEY UPDATE query := `INSERT INTO rulesets (ruleset_id, ruleset_name, ruleset_active) VALUES (?, ?, 1) ON DUPLICATE KEY UPDATE ruleset_name = VALUES(ruleset_name), ruleset_active = VALUES(ruleset_active)` _, err = tx.Exec(query, ruleSet.GetID(), ruleSet.GetName()) if err != nil { return fmt.Errorf("error saving rule set: %v", err) } // Delete existing rule details deleteQuery := "DELETE FROM ruleset_details WHERE ruleset_id = ?" _, err = tx.Exec(deleteQuery, ruleSet.GetID()) if err != nil { return fmt.Errorf("error deleting existing rule details: %v", err) } // Insert rule details insertQuery := "INSERT INTO ruleset_details (ruleset_id, rule_category, rule_type, rule_value) VALUES (?, ?, ?, ?)" rules := ruleSet.GetRules() for _, categoryMap := range rules { for _, rule := range categoryMap { if rule.IsValid() { combined := rule.GetCombined() parts := splitCombined(combined) if len(parts) == 2 { _, err = tx.Exec(insertQuery, ruleSet.GetID(), parts[0], parts[1], rule.GetValue()) if err != nil { return fmt.Errorf("error saving rule detail: %v", err) } } } } } return nil } // DeleteRuleSet deletes a rule set from the database func (ds *DatabaseService) DeleteRuleSet(ruleSetID int32) error { if ds.db == nil { return fmt.Errorf("database not initialized") } // Use transaction for atomicity tx, err := ds.db.Begin() if err != nil { return fmt.Errorf("error beginning transaction: %v", err) } defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }() // Delete rule details first (foreign key constraint) _, err = tx.Exec("DELETE FROM ruleset_details WHERE ruleset_id = ?", ruleSetID) if err != nil { return fmt.Errorf("error deleting rule details: %v", err) } // Delete rule set _, err = tx.Exec("DELETE FROM rulesets WHERE ruleset_id = ?", ruleSetID) if err != nil { return fmt.Errorf("error deleting rule set: %v", err) } return nil } // SetDefaultRuleSet sets the default rule set ID in the variables table func (ds *DatabaseService) SetDefaultRuleSet(ruleSetID int32) error { if ds.db == nil { return fmt.Errorf("database not initialized") } query := `INSERT INTO variables (variable_name, variable_value, comment) VALUES (?, ?, 'Default ruleset ID') ON DUPLICATE KEY UPDATE variable_value = VALUES(variable_value)` _, err := ds.db.Exec(query, DefaultRuleSetIDVar, strconv.Itoa(int(ruleSetID))) if err != nil { return fmt.Errorf("error setting default rule set: %v", err) } return nil } // GetDefaultRuleSetID gets the default rule set ID from the variables table func (ds *DatabaseService) GetDefaultRuleSetID() (int32, error) { if ds.db == nil { return 0, fmt.Errorf("database not initialized") } query := "SELECT variable_value FROM variables WHERE variable_name = ?" var variableValue string err := ds.db.QueryRow(query, DefaultRuleSetIDVar).Scan(&variableValue) if err != nil { if err == sql.ErrNoRows { return 0, fmt.Errorf("default ruleset ID not found in variables table") } return 0, fmt.Errorf("error querying default ruleset ID: %v", err) } if id, err := strconv.ParseInt(variableValue, 10, 32); err == nil { return int32(id), nil } return 0, fmt.Errorf("invalid ruleset ID format: %s", variableValue) } // GetRuleSetList returns a list of all rule sets func (ds *DatabaseService) GetRuleSetList() ([]RuleSetInfo, error) { if ds.db == nil { return nil, fmt.Errorf("database not initialized") } query := "SELECT ruleset_id, ruleset_name, ruleset_active FROM rulesets ORDER BY ruleset_id" var ruleSets []RuleSetInfo rows, err := ds.db.Query(query) if err != nil { return nil, fmt.Errorf("error querying rule sets: %v", err) } defer rows.Close() for rows.Next() { var info RuleSetInfo var active int err := rows.Scan(&info.ID, &info.Name, &active) if err != nil { return nil, fmt.Errorf("error scanning rule set row: %v", err) } info.Active = active > 0 // Convert int to bool ruleSets = append(ruleSets, info) } if err = rows.Err(); err != nil { return nil, fmt.Errorf("error iterating rule sets: %v", err) } return ruleSets, nil } // ValidateDatabase ensures the required tables exist func (ds *DatabaseService) ValidateDatabase() error { if ds.db == nil { return fmt.Errorf("database not initialized") } tables := []string{"rulesets", "ruleset_details", "variables"} query := "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?" for _, table := range tables { var count int err := ds.db.QueryRow(query, table).Scan(&count) if err != nil { return fmt.Errorf("error checking %s table: %v", table, err) } if count == 0 { return fmt.Errorf("%s table does not exist", table) } } return nil } // RuleSetInfo contains basic information about a rule set type RuleSetInfo struct { ID int32 `json:"id"` Name string `json:"name"` Active bool `json:"active"` } // Helper function to split combined rule string func splitCombined(combined string) []string { for i, char := range combined { if char == ':' { return []string{combined[:i], combined[i+1:]} } } return []string{combined} } // CreateRulesTables creates the necessary database tables for rules func (ds *DatabaseService) CreateRulesTables() error { if ds.db == nil { return fmt.Errorf("database not initialized") } // Create rulesets table createRuleSets := ` CREATE TABLE IF NOT EXISTS rulesets ( ruleset_id INTEGER PRIMARY KEY, ruleset_name VARCHAR(255) NOT NULL UNIQUE, ruleset_active INTEGER NOT NULL DEFAULT 0 )` _, err := ds.db.Exec(createRuleSets) if err != nil { return fmt.Errorf("error creating rulesets table: %v", err) } // Create ruleset_details table createRuleSetDetails := ` CREATE TABLE IF NOT EXISTS ruleset_details ( id INTEGER PRIMARY KEY AUTO_INCREMENT, ruleset_id INTEGER NOT NULL, rule_category VARCHAR(255) NOT NULL, rule_type VARCHAR(255) NOT NULL, rule_value TEXT NOT NULL, description TEXT, FOREIGN KEY (ruleset_id) REFERENCES rulesets(ruleset_id) ON DELETE CASCADE )` _, err = ds.db.Exec(createRuleSetDetails) if err != nil { return fmt.Errorf("error creating ruleset_details table: %v", err) } // Create variables table if it doesn't exist createVariables := ` CREATE TABLE IF NOT EXISTS variables ( variable_name VARCHAR(255) PRIMARY KEY, variable_value TEXT NOT NULL, comment TEXT )` _, err = ds.db.Exec(createVariables) if err != nil { return fmt.Errorf("error creating variables table: %v", err) } // Create indexes for better performance indexes := []string{ "CREATE INDEX IF NOT EXISTS idx_ruleset_details_ruleset_id ON ruleset_details(ruleset_id)", "CREATE INDEX IF NOT EXISTS idx_ruleset_details_category_type ON ruleset_details(rule_category, rule_type)", "CREATE INDEX IF NOT EXISTS idx_rulesets_active ON rulesets(ruleset_active)", } for _, indexSQL := range indexes { _, err = ds.db.Exec(indexSQL) if err != nil { return fmt.Errorf("error creating index: %v", err) } } return nil }