import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import '../models/session.dart'; import '../models/shot.dart'; import '../../core/constants/app_constants.dart'; class DatabaseHelper { static DatabaseHelper? _instance; static Database? _database; DatabaseHelper._internal(); factory DatabaseHelper() { _instance ??= DatabaseHelper._internal(); return _instance!; } Future get database async { _database ??= await _initDatabase(); return _database!; } Future _initDatabase() async { final databasesPath = await getDatabasesPath(); final path = join(databasesPath, AppConstants.databaseName); return await openDatabase( path, version: AppConstants.databaseVersion, onCreate: _onCreate, onUpgrade: _onUpgrade, ); } Future _onCreate(Database db, int version) async { await db.execute(''' CREATE TABLE ${AppConstants.sessionsTable} ( id TEXT PRIMARY KEY, target_type TEXT NOT NULL, image_path TEXT NOT NULL, total_score INTEGER NOT NULL, grouping_diameter REAL, grouping_center_x REAL, grouping_center_y REAL, created_at TEXT NOT NULL, notes TEXT, target_center_x REAL, target_center_y REAL, target_radius REAL ) '''); await db.execute(''' CREATE TABLE ${AppConstants.shotsTable} ( id TEXT PRIMARY KEY, x REAL NOT NULL, y REAL NOT NULL, score INTEGER NOT NULL, session_id TEXT NOT NULL, FOREIGN KEY (session_id) REFERENCES ${AppConstants.sessionsTable}(id) ON DELETE CASCADE ) '''); await db.execute(''' CREATE INDEX idx_shots_session_id ON ${AppConstants.shotsTable}(session_id) '''); await db.execute(''' CREATE INDEX idx_sessions_created_at ON ${AppConstants.sessionsTable}(created_at DESC) '''); } Future _onUpgrade(Database db, int oldVersion, int newVersion) async { // Handle future database migrations here } // Session operations Future insertSession(Session session) async { final db = await database; return await db.transaction((txn) async { await txn.insert( AppConstants.sessionsTable, session.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); for (final shot in session.shots) { // Ensure shot has correct session_id final shotWithSessionId = shot.copyWith(sessionId: session.id); await txn.insert( AppConstants.shotsTable, shotWithSessionId.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } return 1; }); } Future getSession(String id) async { final db = await database; final sessionMaps = await db.query( AppConstants.sessionsTable, where: 'id = ?', whereArgs: [id], ); if (sessionMaps.isEmpty) return null; final shotMaps = await db.query( AppConstants.shotsTable, where: 'session_id = ?', whereArgs: [id], ); final shots = shotMaps.map((map) => Shot.fromMap(map)).toList(); return Session.fromMap(sessionMaps.first, shots); } Future> getAllSessions({ String? targetType, int? limit, int? offset, }) async { final db = await database; String? whereClause; List? whereArgs; if (targetType != null) { whereClause = 'target_type = ?'; whereArgs = [targetType]; } final sessionMaps = await db.query( AppConstants.sessionsTable, where: whereClause, whereArgs: whereArgs, orderBy: 'created_at DESC', limit: limit, offset: offset, ); // First, check if there are orphaned shots (with empty session_id) // and only one session - if so, assign them to that session if (sessionMaps.length == 1) { final orphanedShots = await db.query( AppConstants.shotsTable, where: 'session_id = ?', whereArgs: [''], ); if (orphanedShots.isNotEmpty) { await db.update( AppConstants.shotsTable, {'session_id': sessionMaps.first['id']}, where: 'session_id = ?', whereArgs: [''], ); } } final sessions = []; for (final sessionMap in sessionMaps) { final sessionId = sessionMap['id'] as String; final shotMaps = await db.query( AppConstants.shotsTable, where: 'session_id = ?', whereArgs: [sessionId], ); final shots = shotMaps.map((map) => Shot.fromMap(map)).toList(); sessions.add(Session.fromMap(sessionMap, shots)); } return sessions; } Future updateSession(Session session) async { final db = await database; return await db.transaction((txn) async { await txn.update( AppConstants.sessionsTable, session.toMap(), where: 'id = ?', whereArgs: [session.id], ); // Delete existing shots and insert new ones await txn.delete( AppConstants.shotsTable, where: 'session_id = ?', whereArgs: [session.id], ); for (final shot in session.shots) { // Ensure shot has correct session_id final shotWithSessionId = shot.copyWith(sessionId: session.id); await txn.insert( AppConstants.shotsTable, shotWithSessionId.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } return 1; }); } Future deleteSession(String id) async { final db = await database; return await db.delete( AppConstants.sessionsTable, where: 'id = ?', whereArgs: [id], ); } Future> getStatistics() async { final db = await database; final totalSessions = Sqflite.firstIntValue( await db.rawQuery('SELECT COUNT(*) FROM ${AppConstants.sessionsTable}'), ) ?? 0; final totalShots = Sqflite.firstIntValue( await db.rawQuery('SELECT COUNT(*) FROM ${AppConstants.shotsTable}'), ) ?? 0; final avgScore = (await db.rawQuery( 'SELECT AVG(total_score) as avg FROM ${AppConstants.sessionsTable}', )).first['avg'] as double? ?? 0.0; final bestScore = Sqflite.firstIntValue( await db.rawQuery( 'SELECT MAX(total_score) FROM ${AppConstants.sessionsTable}', ), ) ?? 0; return { 'totalSessions': totalSessions, 'totalShots': totalShots, 'averageScore': avgScore, 'bestScore': bestScore, }; } Future close() async { final db = await database; await db.close(); _database = null; } }