package collections import ( "context" "fmt" "eq2emu/internal/database" ) // DatabaseCollectionManager implements CollectionDatabase interface using the existing database wrapper type DatabaseCollectionManager struct { db *database.DB } // NewDatabaseCollectionManager creates a new database collection manager func NewDatabaseCollectionManager(db *database.DB) *DatabaseCollectionManager { return &DatabaseCollectionManager{ db: db, } } // LoadCollections retrieves all collections from database func (dcm *DatabaseCollectionManager) LoadCollections(ctx context.Context) ([]CollectionData, error) { query := "SELECT `id`, `collection_name`, `collection_category`, `level` FROM `collections`" rows, err := dcm.db.QueryContext(ctx, query) if err != nil { return nil, fmt.Errorf("failed to query collections: %w", err) } defer rows.Close() var collections []CollectionData for rows.Next() { var collection CollectionData err := rows.Scan( &collection.ID, &collection.Name, &collection.Category, &collection.Level, ) if err != nil { return nil, fmt.Errorf("failed to scan collection row: %w", err) } collections = append(collections, collection) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating collection rows: %w", err) } return collections, nil } // LoadCollectionItems retrieves items for a specific collection func (dcm *DatabaseCollectionManager) LoadCollectionItems(ctx context.Context, collectionID int32) ([]CollectionItem, error) { query := `SELECT item_id, item_index FROM collection_details WHERE collection_id = ? ORDER BY item_index ASC` rows, err := dcm.db.QueryContext(ctx, query, collectionID) if err != nil { return nil, fmt.Errorf("failed to query collection items for collection %d: %w", collectionID, err) } defer rows.Close() var items []CollectionItem for rows.Next() { var item CollectionItem err := rows.Scan( &item.ItemID, &item.Index, ) if err != nil { return nil, fmt.Errorf("failed to scan collection item row: %w", err) } // Items start as not found item.Found = ItemNotFound items = append(items, item) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating collection item rows: %w", err) } return items, nil } // LoadCollectionRewards retrieves rewards for a specific collection func (dcm *DatabaseCollectionManager) LoadCollectionRewards(ctx context.Context, collectionID int32) ([]CollectionRewardData, error) { query := `SELECT collection_id, reward_type, reward_value, reward_quantity FROM collection_rewards WHERE collection_id = ?` rows, err := dcm.db.QueryContext(ctx, query, collectionID) if err != nil { return nil, fmt.Errorf("failed to query collection rewards for collection %d: %w", collectionID, err) } defer rows.Close() var rewards []CollectionRewardData for rows.Next() { var reward CollectionRewardData err := rows.Scan( &reward.CollectionID, &reward.RewardType, &reward.RewardValue, &reward.Quantity, ) if err != nil { return nil, fmt.Errorf("failed to scan collection reward row: %w", err) } rewards = append(rewards, reward) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating collection reward rows: %w", err) } return rewards, nil } // LoadPlayerCollections retrieves player's collection progress func (dcm *DatabaseCollectionManager) LoadPlayerCollections(ctx context.Context, characterID int32) ([]PlayerCollectionData, error) { query := `SELECT char_id, collection_id, completed FROM character_collections WHERE char_id = ?` rows, err := dcm.db.QueryContext(ctx, query, characterID) if err != nil { return nil, fmt.Errorf("failed to query player collections for character %d: %w", characterID, err) } defer rows.Close() var collections []PlayerCollectionData for rows.Next() { var collection PlayerCollectionData var completed int err := rows.Scan( &collection.CharacterID, &collection.CollectionID, &completed, ) if err != nil { return nil, fmt.Errorf("failed to scan player collection row: %w", err) } collection.Completed = completed == 1 collections = append(collections, collection) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating player collection rows: %w", err) } return collections, nil } // LoadPlayerCollectionItems retrieves player's found collection items func (dcm *DatabaseCollectionManager) LoadPlayerCollectionItems(ctx context.Context, characterID, collectionID int32) ([]int32, error) { query := `SELECT collection_item_id FROM character_collection_items WHERE char_id = ? AND collection_id = ?` rows, err := dcm.db.QueryContext(ctx, query, characterID, collectionID) if err != nil { return nil, fmt.Errorf("failed to query player collection items for character %d, collection %d: %w", characterID, collectionID, err) } defer rows.Close() var itemIDs []int32 for rows.Next() { var itemID int32 err := rows.Scan(&itemID) if err != nil { return nil, fmt.Errorf("failed to scan player collection item row: %w", err) } itemIDs = append(itemIDs, itemID) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating player collection item rows: %w", err) } return itemIDs, nil } // SavePlayerCollection saves player collection completion status func (dcm *DatabaseCollectionManager) SavePlayerCollection(ctx context.Context, characterID, collectionID int32, completed bool) error { completedInt := 0 if completed { completedInt = 1 } query := `INSERT INTO character_collections (char_id, collection_id, completed) VALUES (?, ?, ?) ON CONFLICT(char_id, collection_id) DO UPDATE SET completed = ?` _, err := dcm.db.ExecContext(ctx, query, characterID, collectionID, completedInt, completedInt) if err != nil { return fmt.Errorf("failed to save player collection for character %d, collection %d: %w", characterID, collectionID, err) } return nil } // SavePlayerCollectionItem saves a found collection item func (dcm *DatabaseCollectionManager) SavePlayerCollectionItem(ctx context.Context, characterID, collectionID, itemID int32) error { query := `INSERT OR IGNORE INTO character_collection_items (char_id, collection_id, collection_item_id) VALUES (?, ?, ?)` _, err := dcm.db.ExecContext(ctx, query, characterID, collectionID, itemID) if err != nil { return fmt.Errorf("failed to save player collection item for character %d, collection %d, item %d: %w", characterID, collectionID, itemID, err) } return nil } // SavePlayerCollections saves all modified player collections func (dcm *DatabaseCollectionManager) SavePlayerCollections(ctx context.Context, characterID int32, collections []*Collection) error { if len(collections) == 0 { return nil } // Use a transaction for atomic updates tx, err := dcm.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() for _, collection := range collections { if !collection.GetSaveNeeded() { continue } // Save collection completion status if err := dcm.savePlayerCollectionTx(ctx, tx, characterID, collection); err != nil { return fmt.Errorf("failed to save collection %d: %w", collection.GetID(), err) } // Save found items if err := dcm.savePlayerCollectionItemsTx(ctx, tx, characterID, collection); err != nil { return fmt.Errorf("failed to save collection items for collection %d: %w", collection.GetID(), err) } } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } return nil } // savePlayerCollectionTx saves a single collection within a transaction func (dcm *DatabaseCollectionManager) savePlayerCollectionTx(ctx context.Context, tx database.Tx, characterID int32, collection *Collection) error { completedInt := 0 if collection.GetCompleted() { completedInt = 1 } query := `INSERT INTO character_collections (char_id, collection_id, completed) VALUES (?, ?, ?) ON CONFLICT(char_id, collection_id) DO UPDATE SET completed = ?` _, err := tx.ExecContext(ctx, query, characterID, collection.GetID(), completedInt, completedInt) return err } // savePlayerCollectionItemsTx saves collection items within a transaction func (dcm *DatabaseCollectionManager) savePlayerCollectionItemsTx(ctx context.Context, tx database.Tx, characterID int32, collection *Collection) error { items := collection.GetCollectionItems() for _, item := range items { if item.Found == ItemFound { query := `INSERT OR IGNORE INTO character_collection_items (char_id, collection_id, collection_item_id) VALUES (?, ?, ?)` _, err := tx.ExecContext(ctx, query, characterID, collection.GetID(), item.ItemID) if err != nil { return fmt.Errorf("failed to save item %d: %w", item.ItemID, err) } } } return nil } // EnsureCollectionTables creates the collection tables if they don't exist func (dcm *DatabaseCollectionManager) EnsureCollectionTables(ctx context.Context) error { queries := []string{ `CREATE TABLE IF NOT EXISTS collections ( id INTEGER PRIMARY KEY, collection_name TEXT NOT NULL, collection_category TEXT NOT NULL DEFAULT '', level INTEGER NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, `CREATE TABLE IF NOT EXISTS collection_details ( collection_id INTEGER NOT NULL, item_id INTEGER NOT NULL, item_index INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (collection_id, item_id), FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE )`, `CREATE TABLE IF NOT EXISTS collection_rewards ( id INTEGER PRIMARY KEY AUTOINCREMENT, collection_id INTEGER NOT NULL, reward_type TEXT NOT NULL, reward_value TEXT NOT NULL, reward_quantity INTEGER NOT NULL DEFAULT 1, FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE )`, `CREATE TABLE IF NOT EXISTS character_collections ( char_id INTEGER NOT NULL, collection_id INTEGER NOT NULL, completed INTEGER NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (char_id, collection_id), FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE )`, `CREATE TABLE IF NOT EXISTS character_collection_items ( char_id INTEGER NOT NULL, collection_id INTEGER NOT NULL, collection_item_id INTEGER NOT NULL, found_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (char_id, collection_id, collection_item_id), FOREIGN KEY (char_id, collection_id) REFERENCES character_collections(char_id, collection_id) ON DELETE CASCADE )`, } for i, query := range queries { _, err := dcm.db.ExecContext(ctx, query) if err != nil { return fmt.Errorf("failed to create collection table %d: %w", i+1, err) } } // Create indexes for better performance indexes := []string{ `CREATE INDEX IF NOT EXISTS idx_collection_details_collection_id ON collection_details(collection_id)`, `CREATE INDEX IF NOT EXISTS idx_collection_rewards_collection_id ON collection_rewards(collection_id)`, `CREATE INDEX IF NOT EXISTS idx_character_collections_char_id ON character_collections(char_id)`, `CREATE INDEX IF NOT EXISTS idx_character_collection_items_char_id ON character_collection_items(char_id)`, `CREATE INDEX IF NOT EXISTS idx_character_collection_items_collection_id ON character_collection_items(collection_id)`, `CREATE INDEX IF NOT EXISTS idx_collections_category ON collections(collection_category)`, `CREATE INDEX IF NOT EXISTS idx_collections_level ON collections(level)`, } for i, query := range indexes { _, err := dcm.db.ExecContext(ctx, query) if err != nil { return fmt.Errorf("failed to create collection index %d: %w", i+1, err) } } return nil } // GetCollectionCount returns the total number of collections in the database func (dcm *DatabaseCollectionManager) GetCollectionCount(ctx context.Context) (int, error) { query := "SELECT COUNT(*) FROM collections" var count int err := dcm.db.QueryRowContext(ctx, query).Scan(&count) if err != nil { return 0, fmt.Errorf("failed to get collection count: %w", err) } return count, nil } // GetPlayerCollectionCount returns the number of collections a player has func (dcm *DatabaseCollectionManager) GetPlayerCollectionCount(ctx context.Context, characterID int32) (int, error) { query := "SELECT COUNT(*) FROM character_collections WHERE char_id = ?" var count int err := dcm.db.QueryRowContext(ctx, query, characterID).Scan(&count) if err != nil { return 0, fmt.Errorf("failed to get player collection count for character %d: %w", characterID, err) } return count, nil } // GetCompletedCollectionCount returns the number of completed collections for a player func (dcm *DatabaseCollectionManager) GetCompletedCollectionCount(ctx context.Context, characterID int32) (int, error) { query := "SELECT COUNT(*) FROM character_collections WHERE char_id = ? AND completed = 1" var count int err := dcm.db.QueryRowContext(ctx, query, characterID).Scan(&count) if err != nil { return 0, fmt.Errorf("failed to get completed collection count for character %d: %w", characterID, err) } return count, nil } // DeletePlayerCollection removes a player's collection progress func (dcm *DatabaseCollectionManager) DeletePlayerCollection(ctx context.Context, characterID, collectionID int32) error { // Use a transaction to ensure both tables are updated atomically tx, err := dcm.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Delete collection items first due to foreign key constraint _, err = tx.ExecContext(ctx, "DELETE FROM character_collection_items WHERE char_id = ? AND collection_id = ?", characterID, collectionID) if err != nil { return fmt.Errorf("failed to delete player collection items: %w", err) } // Delete collection _, err = tx.ExecContext(ctx, "DELETE FROM character_collections WHERE char_id = ? AND collection_id = ?", characterID, collectionID) if err != nil { return fmt.Errorf("failed to delete player collection: %w", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } return nil } // GetCollectionStatistics returns database-level collection statistics func (dcm *DatabaseCollectionManager) GetCollectionStatistics(ctx context.Context) (CollectionStatistics, error) { var stats CollectionStatistics // Total collections err := dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM collections").Scan(&stats.TotalCollections) if err != nil { return stats, fmt.Errorf("failed to get total collections: %w", err) } // Total collection items err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM collection_details").Scan(&stats.TotalItems) if err != nil { return stats, fmt.Errorf("failed to get total items: %w", err) } // Players with collections err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(DISTINCT char_id) FROM character_collections").Scan(&stats.PlayersWithCollections) if err != nil { return stats, fmt.Errorf("failed to get players with collections: %w", err) } // Completed collections across all players err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM character_collections WHERE completed = 1").Scan(&stats.CompletedCollections) if err != nil { return stats, fmt.Errorf("failed to get completed collections: %w", err) } // Active collections (incomplete with at least one item found) across all players query := `SELECT COUNT(DISTINCT cc.char_id, cc.collection_id) FROM character_collections cc JOIN character_collection_items cci ON cc.char_id = cci.char_id AND cc.collection_id = cci.collection_id WHERE cc.completed = 0` err = dcm.db.QueryRowContext(ctx, query).Scan(&stats.ActiveCollections) if err != nil { return stats, fmt.Errorf("failed to get active collections: %w", err) } // Found items across all players err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM character_collection_items").Scan(&stats.FoundItems) if err != nil { return stats, fmt.Errorf("failed to get found items: %w", err) } // Total rewards err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM collection_rewards").Scan(&stats.TotalRewards) if err != nil { return stats, fmt.Errorf("failed to get total rewards: %w", err) } return stats, nil }