import 'dart:async'; import 'dart:io'; import 'package:xp_nix/src/models/activity_event.dart'; enum LogLevel { debug(0, 'DEBUG'), info(1, 'INFO'), warn(2, 'WARN'), error(3, 'ERROR'); const LogLevel(this.value, this.name); final int value; final String name; } class Logger { static Logger? _instance; static Logger get instance => _instance ??= Logger._(); Logger._(); LogLevel _currentLevel = LogLevel.info; String _logDirectory = 'logs'; int _maxFileSizeMB = 10; int _maxFiles = 5; File? _currentLogFile; final StreamController _logStreamController = StreamController.broadcast(); Stream get logStream => _logStreamController.stream; Future initialize({ LogLevel level = LogLevel.info, String logDirectory = 'logs', int maxFileSizeMB = 10, int maxFiles = 5, }) async { _currentLevel = level; _logDirectory = logDirectory; _maxFileSizeMB = maxFileSizeMB; _maxFiles = maxFiles; await _setupLogFile(); _info('Logger initialized with level: ${level.name}'); } Future _setupLogFile() async { final logDir = Directory(_logDirectory); if (!await logDir.exists()) { await logDir.create(recursive: true); } final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-').split('.')[0]; _currentLogFile = File('$_logDirectory/xp_nix_$timestamp.log'); // Create the file if it doesn't exist if (!await _currentLogFile!.exists()) { await _currentLogFile!.create(); } } Future _rotateLogIfNeeded() async { if (_currentLogFile == null) return; try { final stats = await _currentLogFile!.stat(); final fileSizeMB = stats.size / (1024 * 1024); if (fileSizeMB > _maxFileSizeMB) { await _cleanupOldLogs(); await _setupLogFile(); _info('Log file rotated due to size limit'); } } catch (e) { // If we can't check file size, just continue print('Could not check log file size: $e'); } } Future _cleanupOldLogs() async { final logDir = Directory(_logDirectory); if (!await logDir.exists()) return; try { final logFiles = await logDir.list().where((entity) => entity is File && entity.path.endsWith('.log')).cast().toList(); logFiles.sort((a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync())); if (logFiles.length >= _maxFiles) { for (int i = _maxFiles - 1; i < logFiles.length; i++) { _info('Deleting old log file ${logFiles[i].toString()}'); await logFiles[i].delete(); } } } catch (e) { print('Could not cleanup old logs: $e'); } } void _log(LogLevel level, String message, [Object? error, StackTrace? stackTrace]) { if (level.value < _currentLevel.value) return; final timestamp = DateTime.now().toIso8601String(); final logEntry = '[$timestamp] [${level.name}] $message'; // Print to console print(logEntry); // Add to stream for real-time monitoring _logStreamController.add(logEntry); // Write to file asynchronously _writeToFileAsync(logEntry, error, stackTrace); } void _writeToFileAsync(String logEntry, [Object? error, StackTrace? stackTrace]) { // Use Future.microtask to avoid blocking the current execution Future.microtask(() async { await _writeToFile(logEntry, error, stackTrace); }); } Future _writeToFile(String logEntry, [Object? error, StackTrace? stackTrace]) async { if (_currentLogFile == null) return; try { final buffer = StringBuffer(); buffer.writeln(logEntry); if (error != null) { buffer.writeln('Error: $error'); } if (stackTrace != null) { buffer.writeln('Stack trace: $stackTrace'); } // Write to file using writeAsString with append mode await _currentLogFile!.writeAsString(buffer.toString(), mode: FileMode.append, flush: true); // Check if rotation is needed await _rotateLogIfNeeded(); } catch (e) { // If writing fails, try to recreate the log file print('Log writing failed: $e'); try { await _setupLogFile(); } catch (setupError) { print('Could not recreate log file: $setupError'); } } } // Static convenience methods static void debug(String message) => instance._log(LogLevel.debug, message); static void info(String message) => instance._log(LogLevel.info, message); static void warn(String message) => instance._log(LogLevel.warn, message); static void error(String message, [Object? error, StackTrace? stackTrace]) => instance._log(LogLevel.error, message, error, stackTrace); // Instance methods for when you have a logger instance // ignore: unused_element void _debug(String message) => _log(LogLevel.debug, message); // ignore: unused_element void _info(String message) => _log(LogLevel.info, message); // ignore: unused_element void _warn(String message) => _log(LogLevel.warn, message); // ignore: unused_element void _error(String message, [Object? error, StackTrace? stackTrace]) => _log(LogLevel.error, message, error, stackTrace); // Activity-specific logging methods static void logActivity(String activityType, String application, int durationSeconds, int xpGained) { info('ACTIVITY: $activityType in $application for ${durationSeconds}s (+$xpGained XP)'); } static void logFocusSession(int minutes, int bonusXP) { info('FOCUS_SESSION: ${minutes}min session completed (+$bonusXP XP)'); } static void logLevelUp(int oldLevel, int newLevel, int totalXP) { info('LEVEL_UP: $oldLevel → $newLevel (Total XP: $totalXP)'); } static void logAchievement(String name, String description, int xpReward) { info('ACHIEVEMENT: $name - $description (+$xpReward XP)'); } static void logConfigChange(String path, dynamic oldValue, dynamic newValue) { info('CONFIG_CHANGE: $path changed from $oldValue to $newValue'); } static void logXPCalculation(ActivityEventType activityType, int baseXP, double multiplier, int finalXP) { debug('XP_CALC: ${activityType.displayName} base=$baseXP × $multiplier = $finalXP'); } static void logIdleStateChange(bool isIdle, Duration? idleDuration) { if (isIdle) { debug('IDLE: User became idle'); } else { final duration = idleDuration?.inMinutes ?? 0; info('ACTIVE: User became active after ${duration}min idle'); } } static void logZoomStatusChange(String oldStatus, String newStatus) { info('ZOOM_STATUS: $oldStatus → $newStatus'); } static void logThemeChange(int level, String themeName) { info('THEME_CHANGE: Applied theme "$themeName" for level $level'); } static void logXPGain(String source, int xpGained, String activity, int currentXP, int currentLevel) { info('XP_GAIN: +$xpGained from $source ($activity) - Total: $currentXP XP, Level: $currentLevel'); } // Performance logging static void logPerformanceMetric(String operation, Duration duration) { debug('PERFORMANCE: $operation took ${duration.inMilliseconds}ms'); } // Get recent log entries for dashboard Future> getRecentLogs([int count = 100]) async { if (_currentLogFile == null || !await _currentLogFile!.exists()) { return []; } try { final lines = await _currentLogFile!.readAsLines(); return lines.reversed.take(count).toList(); } catch (e) { _error('Failed to read log file: $e'); return []; } } // Get logs by level Future> getLogsByLevel(LogLevel level, [int count = 50]) async { final allLogs = await getRecentLogs(count * 2); return allLogs.where((log) => log.contains('[${level.name}]')).take(count).toList(); } // Update log level dynamically void setLogLevel(LogLevel level) { _currentLevel = level; info('Log level changed to: ${level.name}'); } Future dispose() async { await _logStreamController.close(); } }