import 'package:sqflite/sqflite.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:path/path.dart'; import 'dart:io'; import 'dart:convert'; import '../models/supplement.dart'; import '../models/supplement_intake.dart'; class DatabaseHelper { static const _databaseName = 'supplements.db'; static const _databaseVersion = 5; // Increment version for notification tracking static const supplementsTable = 'supplements'; static const intakesTable = 'supplement_intakes'; static const notificationTrackingTable = 'notification_tracking'; DatabaseHelper._privateConstructor(); static final DatabaseHelper instance = DatabaseHelper._privateConstructor(); static Database? _database; static bool _initialized = false; static void _initializeDatabaseFactory() { if (!_initialized) { // Initialize for desktop platforms if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { sqfliteFfiInit(); databaseFactory = databaseFactoryFfi; } _initialized = true; } } Future get database async { _initializeDatabaseFactory(); _database ??= await _initDatabase(); return _database!; } Future _initDatabase() async { String path = join(await getDatabasesPath(), _databaseName); return await openDatabase( path, version: _databaseVersion, onCreate: _onCreate, onUpgrade: _onUpgrade, ); } Future _onCreate(Database db, int version) async { await db.execute(''' CREATE TABLE $supplementsTable ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, brand TEXT, ingredients TEXT NOT NULL DEFAULT '[]', numberOfUnits INTEGER NOT NULL DEFAULT 1, unitType TEXT NOT NULL DEFAULT 'units', frequencyPerDay INTEGER NOT NULL, reminderTimes TEXT NOT NULL, notes TEXT, createdAt TEXT NOT NULL, isActive INTEGER NOT NULL DEFAULT 1 ) '''); await db.execute(''' CREATE TABLE $intakesTable ( id INTEGER PRIMARY KEY AUTOINCREMENT, supplementId INTEGER NOT NULL, takenAt TEXT NOT NULL, dosageTaken REAL NOT NULL, unitsTaken REAL NOT NULL DEFAULT 1, notes TEXT, FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id) ) '''); await db.execute(''' CREATE TABLE $notificationTrackingTable ( id INTEGER PRIMARY KEY AUTOINCREMENT, notificationId INTEGER NOT NULL UNIQUE, supplementId INTEGER NOT NULL, scheduledTime TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', retryCount INTEGER NOT NULL DEFAULT 0, lastRetryTime TEXT, createdAt TEXT NOT NULL, FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id) ) '''); } Future _onUpgrade(Database db, int oldVersion, int newVersion) async { if (oldVersion < 2) { // First, add new columns await db.execute('ALTER TABLE $supplementsTable ADD COLUMN dosageAmount REAL DEFAULT 0'); await db.execute('ALTER TABLE $supplementsTable ADD COLUMN numberOfUnits INTEGER DEFAULT 1'); await db.execute('ALTER TABLE $supplementsTable ADD COLUMN unitType TEXT DEFAULT "units"'); await db.execute('ALTER TABLE $intakesTable ADD COLUMN unitsTaken REAL DEFAULT 1'); // Migrate existing data from old dosage column to new dosageAmount column await db.execute(''' UPDATE $supplementsTable SET dosageAmount = COALESCE(dosage, 0), numberOfUnits = 1, unitType = 'units' WHERE dosageAmount = 0 '''); // Create new table with correct schema await db.execute(''' CREATE TABLE ${supplementsTable}_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, brand TEXT, dosageAmount REAL NOT NULL, numberOfUnits INTEGER NOT NULL DEFAULT 1, unit TEXT NOT NULL, unitType TEXT NOT NULL DEFAULT 'units', frequencyPerDay INTEGER NOT NULL, reminderTimes TEXT NOT NULL, notes TEXT, createdAt TEXT NOT NULL, isActive INTEGER NOT NULL DEFAULT 1 ) '''); // Copy data to new table await db.execute(''' INSERT INTO ${supplementsTable}_new (id, name, brand, dosageAmount, numberOfUnits, unit, unitType, frequencyPerDay, reminderTimes, notes, createdAt, isActive) SELECT id, name, NULL as brand, dosageAmount, numberOfUnits, unit, unitType, frequencyPerDay, reminderTimes, notes, createdAt, isActive FROM $supplementsTable '''); // Drop old table and rename new table await db.execute('DROP TABLE $supplementsTable'); await db.execute('ALTER TABLE ${supplementsTable}_new RENAME TO $supplementsTable'); } if (oldVersion < 3) { // Add brand column for version 3 await db.execute('ALTER TABLE $supplementsTable ADD COLUMN brand TEXT'); } if (oldVersion < 4) { // Complete migration to new ingredient-based schema // Add ingredients column and migrate old data await db.execute('ALTER TABLE $supplementsTable ADD COLUMN ingredients TEXT DEFAULT "[]"'); // Migrate existing supplements to use ingredients format final supplements = await db.query(supplementsTable); for (final supplement in supplements) { final dosageAmount = supplement['dosageAmount'] as double?; final unit = supplement['unit'] as String?; final name = supplement['name'] as String; if (dosageAmount != null && unit != null && dosageAmount > 0) { // Create a single ingredient from the old dosage data final ingredient = { 'name': name, 'amount': dosageAmount, 'unit': unit, }; final ingredientsJson = jsonEncode([ingredient]); await db.update( supplementsTable, {'ingredients': ingredientsJson}, where: 'id = ?', whereArgs: [supplement['id']], ); } } // Remove old columns await db.execute(''' CREATE TABLE ${supplementsTable}_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, brand TEXT, ingredients TEXT NOT NULL DEFAULT '[]', numberOfUnits INTEGER NOT NULL DEFAULT 1, unitType TEXT NOT NULL DEFAULT 'units', frequencyPerDay INTEGER NOT NULL, reminderTimes TEXT NOT NULL, notes TEXT, createdAt TEXT NOT NULL, isActive INTEGER NOT NULL DEFAULT 1 ) '''); await db.execute(''' INSERT INTO ${supplementsTable}_new (id, name, brand, ingredients, numberOfUnits, unitType, frequencyPerDay, reminderTimes, notes, createdAt, isActive) SELECT id, name, brand, ingredients, numberOfUnits, unitType, frequencyPerDay, reminderTimes, notes, createdAt, isActive FROM $supplementsTable '''); await db.execute('DROP TABLE $supplementsTable'); await db.execute('ALTER TABLE ${supplementsTable}_new RENAME TO $supplementsTable'); } if (oldVersion < 5) { // Add notification tracking table await db.execute(''' CREATE TABLE $notificationTrackingTable ( id INTEGER PRIMARY KEY AUTOINCREMENT, notificationId INTEGER NOT NULL UNIQUE, supplementId INTEGER NOT NULL, scheduledTime TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', retryCount INTEGER NOT NULL DEFAULT 0, lastRetryTime TEXT, createdAt TEXT NOT NULL, FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id) ) '''); } } // Supplement CRUD operations Future insertSupplement(Supplement supplement) async { Database db = await database; return await db.insert(supplementsTable, supplement.toMap()); } Future> getAllSupplements() async { Database db = await database; List> maps = await db.query( supplementsTable, where: 'isActive = ?', whereArgs: [1], orderBy: 'name ASC', ); return List.generate(maps.length, (i) => Supplement.fromMap(maps[i])); } Future> getArchivedSupplements() async { Database db = await database; List> maps = await db.query( supplementsTable, where: 'isActive = ?', whereArgs: [0], orderBy: 'name ASC', ); return List.generate(maps.length, (i) => Supplement.fromMap(maps[i])); } Future archiveSupplement(int id) async { Database db = await database; await db.update( supplementsTable, {'isActive': 0}, where: 'id = ?', whereArgs: [id], ); } Future unarchiveSupplement(int id) async { Database db = await database; await db.update( supplementsTable, {'isActive': 1}, where: 'id = ?', whereArgs: [id], ); } Future getSupplement(int id) async { Database db = await database; List> maps = await db.query( supplementsTable, where: 'id = ?', whereArgs: [id], ); if (maps.isNotEmpty) { return Supplement.fromMap(maps.first); } return null; } Future updateSupplement(Supplement supplement) async { Database db = await database; return await db.update( supplementsTable, supplement.toMap(), where: 'id = ?', whereArgs: [supplement.id], ); } Future deleteSupplement(int id) async { Database db = await database; return await db.update( supplementsTable, {'isActive': 0}, where: 'id = ?', whereArgs: [id], ); } // Supplement Intake CRUD operations Future insertIntake(SupplementIntake intake) async { Database db = await database; return await db.insert(intakesTable, intake.toMap()); } Future> getIntakesForDate(DateTime date) async { Database db = await database; String startDate = DateTime(date.year, date.month, date.day).toIso8601String(); String endDate = DateTime(date.year, date.month, date.day, 23, 59, 59).toIso8601String(); List> maps = await db.query( intakesTable, where: 'takenAt >= ? AND takenAt <= ?', whereArgs: [startDate, endDate], orderBy: 'takenAt DESC', ); return List.generate(maps.length, (i) => SupplementIntake.fromMap(maps[i])); } Future> getIntakesForMonth(int year, int month) async { Database db = await database; String startDate = DateTime(year, month, 1).toIso8601String(); String endDate = DateTime(year, month + 1, 0, 23, 59, 59).toIso8601String(); List> maps = await db.query( intakesTable, where: 'takenAt >= ? AND takenAt <= ?', whereArgs: [startDate, endDate], orderBy: 'takenAt DESC', ); return List.generate(maps.length, (i) => SupplementIntake.fromMap(maps[i])); } Future>> getIntakesWithSupplementsForDate(DateTime date) async { Database db = await database; String startDate = DateTime(date.year, date.month, date.day).toIso8601String(); String endDate = DateTime(date.year, date.month, date.day, 23, 59, 59).toIso8601String(); List> result = await db.rawQuery(''' SELECT i.*, i.supplementId as supplement_id, s.name as supplementName, s.unitType as supplementUnitType FROM $intakesTable i JOIN $supplementsTable s ON i.supplementId = s.id WHERE i.takenAt >= ? AND i.takenAt <= ? ORDER BY i.takenAt DESC ''', [startDate, endDate]); return result; } Future>> getIntakesWithSupplementsForMonth(int year, int month) async { Database db = await database; String startDate = DateTime(year, month, 1).toIso8601String(); String endDate = DateTime(year, month + 1, 0, 23, 59, 59).toIso8601String(); List> result = await db.rawQuery(''' SELECT i.*, i.supplementId as supplement_id, s.name as supplementName, s.unitType as supplementUnitType FROM $intakesTable i JOIN $supplementsTable s ON i.supplementId = s.id WHERE i.takenAt >= ? AND i.takenAt <= ? ORDER BY i.takenAt DESC ''', [startDate, endDate]); return result; } Future deleteIntake(int id) async { Database db = await database; return await db.delete( intakesTable, where: 'id = ?', whereArgs: [id], ); } // Notification tracking methods Future trackNotification({ required int notificationId, required int supplementId, required DateTime scheduledTime, }) async { Database db = await database; // Use INSERT OR REPLACE to handle both new and existing notifications await db.rawInsert(''' INSERT OR REPLACE INTO $notificationTrackingTable (notificationId, supplementId, scheduledTime, status, retryCount, lastRetryTime, createdAt) VALUES (?, ?, ?, ?, ?, ?, ?) ''', [ notificationId, supplementId, scheduledTime.toIso8601String(), 'pending', 0, null, DateTime.now().toIso8601String(), ]); return notificationId; } Future markNotificationTaken(int notificationId) async { Database db = await database; await db.update( notificationTrackingTable, {'status': 'taken'}, where: 'notificationId = ?', whereArgs: [notificationId], ); } Future incrementRetryCount(int notificationId) async { Database db = await database; await db.rawUpdate(''' UPDATE $notificationTrackingTable SET retryCount = retryCount + 1, lastRetryTime = ?, status = 'retrying' WHERE notificationId = ? ''', [DateTime.now().toIso8601String(), notificationId]); } Future>> getPendingNotifications() async { Database db = await database; return await db.rawQuery(''' SELECT nt.*, s.name as supplementName FROM $notificationTrackingTable nt LEFT JOIN $supplementsTable s ON nt.supplementId = s.id WHERE nt.status IN (?, ?) ORDER BY nt.scheduledTime ASC ''', ['pending', 'retrying']); } Future markNotificationExpired(int notificationId) async { Database db = await database; await db.update( notificationTrackingTable, {'status': 'expired'}, where: 'notificationId = ?', whereArgs: [notificationId], ); } Future cleanupOldNotificationTracking() async { Database db = await database; // Remove tracking records older than 7 days final cutoffDate = DateTime.now().subtract(const Duration(days: 7)).toIso8601String(); await db.delete( notificationTrackingTable, where: 'createdAt < ?', whereArgs: [cutoffDate], ); } Future clearNotificationTracking(int supplementId) async { Database db = await database; await db.delete( notificationTrackingTable, where: 'supplementId = ?', whereArgs: [supplementId], ); } }