import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'file_store_support.dart'; import 'runtime_models.dart'; typedef SecureConfigDatabaseOpener = FutureOr Function(String resolvedPath); enum SettingsSnapshotReloadStatus { applied, invalid } class SettingsSnapshotReloadResult { const SettingsSnapshotReloadResult({ required this.snapshot, required this.status, }); final SettingsSnapshot snapshot; final SettingsSnapshotReloadStatus status; bool get applied => status == SettingsSnapshotReloadStatus.applied; } class SettingsStore { SettingsStore({ Future Function()? fallbackDirectoryPathResolver, Future Function()? databasePathResolver, Future Function()? defaultSupportDirectoryPathResolver, SecureConfigDatabaseOpener? databaseOpener, StoreLayoutResolver? layoutResolver, }) : _layoutResolver = layoutResolver ?? StoreLayoutResolver( localRootPathResolver: databasePathResolver, supportRootPathResolver: defaultSupportDirectoryPathResolver, ), _enableUserSettingsMirror = databasePathResolver == null && defaultSupportDirectoryPathResolver == null; static const String settingsKey = 'xworkmate.settings.snapshot'; static const String auditKey = 'xworkmate.secrets.audit'; static const String assistantThreadsKey = 'xworkmate.assistant.threads'; static const String databaseFileName = 'config-store.sqlite3'; static const String databaseTableName = 'config_entries'; final StoreLayoutResolver _layoutResolver; final bool _enableUserSettingsMirror; bool _initialized = false; StoreLayout? _layout; List _settingsFiles = const []; List _settingsWatchDirectories = const []; SettingsSnapshot _settingsSnapshot = SettingsSnapshot.defaults(); List _threadRecords = const []; List _auditTrail = const []; PersistentWriteFailure? _settingsWriteFailure; PersistentWriteFailure? _tasksWriteFailure; PersistentWriteFailure? _auditWriteFailure; PersistentWriteFailure? get settingsWriteFailure => _settingsWriteFailure; PersistentWriteFailure? get tasksWriteFailure => _tasksWriteFailure; PersistentWriteFailure? get auditWriteFailure => _auditWriteFailure; Future initialize() async { if (_initialized) { return; } _initialized = true; try { _layout = await _layoutResolver.resolve(); _settingsFiles = _resolveSettingsFiles(_layout!); _settingsWatchDirectories = _resolveSettingsWatchDirectories( _settingsFiles, ); } catch (_) { _layout = null; _settingsFiles = const []; _settingsWatchDirectories = const []; return; } _settingsSnapshot = await _readSettingsSnapshot(); _threadRecords = await _readAssistantThreadRecords(); _auditTrail = await _readAuditTrail(); } Future loadSettingsSnapshot() async { await initialize(); return _settingsSnapshot; } Future reloadSettingsSnapshot() async { final result = await reloadSettingsSnapshotResult(); return result.snapshot; } Future reloadSettingsSnapshotResult() async { await initialize(); final result = await _readSettingsSnapshotResult(); if (result.status == SettingsSnapshotReloadStatus.invalid) { return SettingsSnapshotReloadResult( snapshot: _settingsSnapshot, status: SettingsSnapshotReloadStatus.invalid, ); } _settingsSnapshot = result.snapshot; return SettingsSnapshotReloadResult( snapshot: _settingsSnapshot, status: SettingsSnapshotReloadStatus.applied, ); } Future> resolvedSettingsFiles() async { await initialize(); return List.from(_settingsFiles); } Future> resolvedSettingsWatchDirectories() async { await initialize(); return List.from(_settingsWatchDirectories); } Future saveSettingsSnapshot(SettingsSnapshot snapshot) async { await initialize(); _settingsSnapshot = snapshot; final layout = _layout; if (layout == null) { _settingsWriteFailure = _buildWriteFailure( PersistentStoreScope.settings, 'saveSettingsSnapshot', StateError('Persistent settings path unavailable; using memory only.'), ); return; } try { final contents = encodeYamlDocument(snapshot.toJson()); for (final file in _settingsFiles.isEmpty ? [layout.settingsFile] : _settingsFiles) { await atomicWriteString(file, contents); } _settingsWriteFailure = null; } catch (error) { _settingsWriteFailure = _buildWriteFailure( PersistentStoreScope.settings, 'saveSettingsSnapshot', error, ); } } Future> loadAssistantThreadRecords() async { await initialize(); return List.from(_threadRecords); } Future saveAssistantThreadRecords( List records, ) async { await initialize(); final normalized = records .where((item) => item.sessionKey.trim().isNotEmpty) .toList(growable: false); _threadRecords = normalized; final layout = _layout; if (layout == null) { _tasksWriteFailure = _buildWriteFailure( PersistentStoreScope.tasks, 'saveAssistantThreadRecords', StateError('Persistent task path unavailable; using memory only.'), ); return; } final keptPaths = {}; try { for (final record in normalized) { final taskFile = layout.taskFileForSessionKey(record.sessionKey); keptPaths.add(taskFile.path); await atomicWriteString(taskFile, jsonEncode(record.toJson())); } await atomicWriteString( layout.taskIndexFile, jsonEncode({ 'version': 1, 'sessions': normalized .map((item) => item.sessionKey) .toList(growable: false), }), ); await for (final entity in layout.tasksDirectory.list()) { if (entity is! File) { continue; } if (entity.path == layout.taskIndexFile.path) { continue; } if (!entity.path.endsWith('.json')) { continue; } if (!keptPaths.contains(entity.path)) { await entity.delete(); } } _tasksWriteFailure = null; } catch (error) { _tasksWriteFailure = _buildWriteFailure( PersistentStoreScope.tasks, 'saveAssistantThreadRecords', error, ); } } Future clearAssistantLocalState() async { await initialize(); _settingsSnapshot = SettingsSnapshot.defaults(); _threadRecords = const []; final layout = _layout; if (layout == null) { _settingsWriteFailure = _buildWriteFailure( PersistentStoreScope.settings, 'clearAssistantLocalState', StateError( 'Persistent settings path unavailable; reset kept in memory.', ), ); _tasksWriteFailure = _buildWriteFailure( PersistentStoreScope.tasks, 'clearAssistantLocalState', StateError('Persistent task path unavailable; reset kept in memory.'), ); return; } try { final settingsFiles = _settingsFiles.isEmpty ? [layout.settingsFile] : _settingsFiles; for (final file in settingsFiles) { await deleteIfExists(file); } _settingsWriteFailure = null; } catch (error) { _settingsWriteFailure = _buildWriteFailure( PersistentStoreScope.settings, 'clearAssistantLocalState', error, ); } try { await deleteIfExists(layout.taskIndexFile); await for (final entity in layout.tasksDirectory.list()) { if (entity is File && entity.path.endsWith('.json')) { await entity.delete(); } } _tasksWriteFailure = null; } catch (error) { _tasksWriteFailure = _buildWriteFailure( PersistentStoreScope.tasks, 'clearAssistantLocalState', error, ); } } Future> loadAuditTrail() async { await initialize(); return List.from(_auditTrail); } Future appendAudit(SecretAuditEntry entry) async { await initialize(); final next = [entry, ..._auditTrail]; if (next.length > 40) { next.removeRange(40, next.length); } _auditTrail = next; final layout = _layout; if (layout == null) { _auditWriteFailure = _buildWriteFailure( PersistentStoreScope.audit, 'appendAudit', StateError('Persistent audit path unavailable; audit kept in memory.'), ); return; } try { await atomicWriteString( layout.auditFile, jsonEncode(next.map((item) => item.toJson()).toList(growable: false)), ); _auditWriteFailure = null; } catch (error) { _auditWriteFailure = _buildWriteFailure( PersistentStoreScope.audit, 'appendAudit', error, ); } } void dispose() {} Future _readSettingsSnapshot() async { final result = await _readSettingsSnapshotResult(); return result.status == SettingsSnapshotReloadStatus.invalid ? SettingsSnapshot.defaults() : result.snapshot; } Future _readSettingsSnapshotResult() async { if (_settingsFiles.isEmpty) { return SettingsSnapshotReloadResult( snapshot: SettingsSnapshot.defaults(), status: SettingsSnapshotReloadStatus.applied, ); } var sawExistingFile = false; var sawInvalidFile = false; for (final file in _settingsFiles) { if (!await file.exists()) { continue; } sawExistingFile = true; try { final raw = await file.readAsString(); final decoded = decodeYamlDocument(raw); if (decoded is Map) { return SettingsSnapshotReloadResult( snapshot: SettingsSnapshot.fromJson( decoded.cast(), ), status: SettingsSnapshotReloadStatus.applied, ); } sawInvalidFile = true; } catch (_) { sawInvalidFile = true; } } return SettingsSnapshotReloadResult( snapshot: SettingsSnapshot.defaults(), status: sawExistingFile && sawInvalidFile ? SettingsSnapshotReloadStatus.invalid : SettingsSnapshotReloadStatus.applied, ); } List _resolveSettingsFiles(StoreLayout layout) { final resolved = []; final seen = {}; void addPath(String path) { final normalized = path.trim(); if (normalized.isEmpty || !seen.add(normalized)) { return; } resolved.add(File(normalized)); } final userPath = _enableUserSettingsMirror ? defaultUserSettingsFilePath() : null; if ((userPath ?? '').isNotEmpty) { addPath(userPath!); } addPath(layout.settingsFile.path); return List.unmodifiable(resolved); } List _resolveSettingsWatchDirectories(List files) { final directories = []; final seen = {}; for (final file in files) { final path = file.parent.path.trim(); if (path.isEmpty || !seen.add(path)) { continue; } directories.add(Directory(path)); } return List.unmodifiable(directories); } Future> _readAssistantThreadRecords() async { final layout = _layout; if (layout == null) { return const []; } final orderedKeys = await _readThreadIndex(layout); final recordsByKey = {}; try { await for (final entity in layout.tasksDirectory.list()) { if (entity is! File || entity.path == layout.taskIndexFile.path || !entity.path.endsWith('.json')) { continue; } try { final raw = await entity.readAsString(); final decoded = jsonDecode(raw); if (decoded is Map) { final record = AssistantThreadRecord.fromJson(decoded); if (record.sessionKey.trim().isNotEmpty) { recordsByKey[record.sessionKey] = record; } } } catch (_) { continue; } } } catch (_) { return const []; } final ordered = []; for (final sessionKey in orderedKeys) { final record = recordsByKey.remove(sessionKey); if (record != null) { ordered.add(record); } } final leftovers = recordsByKey.keys.toList()..sort(); for (final sessionKey in leftovers) { final record = recordsByKey[sessionKey]; if (record != null) { ordered.add(record); } } return ordered; } Future> _readThreadIndex(StoreLayout layout) async { if (!await layout.taskIndexFile.exists()) { return const []; } try { final raw = await layout.taskIndexFile.readAsString(); final decoded = jsonDecode(raw); if (decoded is Map) { final sessions = decoded['sessions']; if (sessions is List) { return sessions .map((item) => item.toString().trim()) .where((item) => item.isNotEmpty) .toList(growable: false); } } } catch (_) {} return const []; } Future> _readAuditTrail() async { final layout = _layout; if (layout == null || !await layout.auditFile.exists()) { return const []; } try { final raw = await layout.auditFile.readAsString(); final decoded = jsonDecode(raw); if (decoded is List) { return decoded .whereType() .map( (item) => SecretAuditEntry.fromJson(item.cast()), ) .toList(growable: false); } } catch (_) {} return const []; } PersistentWriteFailure _buildWriteFailure( PersistentStoreScope scope, String operation, Object error, ) { return PersistentWriteFailure( scope: scope, operation: operation, message: error.toString(), timestampMs: DateTime.now().millisecondsSinceEpoch, ); } }