import 'dart:convert'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:json_schema/json_schema.dart'; import 'package:provider/provider.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/dto/game.dart'; import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/dto/player.dart'; import 'package:tallee/data/dto/team.dart'; class DataTransferService { /// Deletes all data from the database. static Future deleteAllData(BuildContext context) async { final db = Provider.of(context, listen: false); await db.matchDao.deleteAllMatches(); await db.teamDao.deleteAllTeams(); await db.groupDao.deleteAllGroups(); await db.gameDao.deleteAllGames(); await db.playerDao.deleteAllPlayers(); } /// Retrieves all application data and converts it to a JSON string. /// Returns the JSON string representation of the data in normalized format. static Future getAppDataAsJson(BuildContext context) async { final db = Provider.of(context, listen: false); final matches = await db.matchDao.getAllMatches(); final groups = await db.groupDao.getAllGroups(); final players = await db.playerDao.getAllPlayers(); final games = await db.gameDao.getAllGames(); final teams = await db.teamDao.getAllTeams(); // Construct a JSON representation of the data in normalized format final Map jsonMap = { 'players': players.map((p) => p.toJson()).toList(), 'games': games.map((g) => g.toJson()).toList(), 'groups': groups .map((g) => { 'id': g.id, 'name': g.name, 'description': g.description, 'createdAt': g.createdAt.toIso8601String(), 'memberIds': (g.members).map((m) => m.id).toList(), }) .toList(), 'teams': teams .map((t) => { 'id': t.id, 'name': t.name, 'createdAt': t.createdAt.toIso8601String(), 'memberIds': (t.members).map((m) => m.id).toList(), }) .toList(), 'matches': matches .map((m) => { 'id': m.id, 'name': m.name, 'createdAt': m.createdAt.toIso8601String(), 'endedAt': m.endedAt?.toIso8601String(), 'gameId': m.game.id, 'groupId': m.group?.id, 'playerIds': (m.players ?? []).map((p) => p.id).toList(), 'notes': m.notes, }) .toList(), }; return json.encode(jsonMap); } /// Exports the given JSON string to a file with the specified name. /// Returns an [ExportResult] indicating the outcome. /// /// [jsonString] The JSON string to be exported. /// [fileName] The desired name for the exported file (without extension). static Future exportData( String jsonString, String fileName ) async { try { final bytes = Uint8List.fromList(utf8.encode(jsonString)); final path = await FilePicker.platform.saveFile( fileName: '$fileName.json', bytes: bytes, ); if (path == null) { return ExportResult.canceled; } else { return ExportResult.success; } } catch (e, stack) { print('[exportData] $e'); print(stack); return ExportResult.unknownException; } } /// Imports data from a selected JSON file into the database. static Future importData(BuildContext context) async { final db = Provider.of(context, listen: false); final path = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['json'], ); if (path == null) { return ImportResult.canceled; } try { final jsonString = await _readFileContent(path.files.single); if (jsonString == null) return ImportResult.fileReadError; final isValid = await _validateJsonSchema(jsonString); if (!isValid) return ImportResult.invalidSchema; final Map decoded = json.decode(jsonString) as Map; final List playersJson = (decoded['players'] as List?) ?? []; final List gamesJson = (decoded['games'] as List?) ?? []; final List groupsJson = (decoded['groups'] as List?) ?? []; final List teamsJson = (decoded['teams'] as List?) ?? []; final List matchesJson = (decoded['matches'] as List?) ?? []; // Import Players final List importedPlayers = playersJson .map((p) => Player.fromJson(p as Map)) .toList(); final Map playerById = { for (final p in importedPlayers) p.id: p, }; // Import Games final List importedGames = gamesJson .map((g) => Game.fromJson(g as Map)) .toList(); final Map gameById = { for (final g in importedGames) g.id: g, }; // Import Groups final List importedGroups = groupsJson.map((g) { final map = g as Map; final memberIds = (map['memberIds'] as List? ?? []).cast(); final members = memberIds .map((id) => playerById[id]) .whereType() .toList(); return Group( id: map['id'] as String, name: map['name'] as String, description: map['description'] as String, members: members, createdAt: DateTime.parse(map['createdAt'] as String), ); }).toList(); final Map groupById = { for (final g in importedGroups) g.id: g, }; // Import Teams final List importedTeams = teamsJson.map((t) { final map = t as Map; final memberIds = (map['memberIds'] as List? ?? []).cast(); final members = memberIds .map((id) => playerById[id]) .whereType() .toList(); return Team( id: map['id'] as String, name: map['name'] as String, members: members, createdAt: DateTime.parse(map['createdAt'] as String), ); }).toList(); // Import Matches final List importedMatches = matchesJson.map((m) { final map = m as Map; final String gameId = map['gameId'] as String; final String? groupId = map['groupId'] as String?; final List playerIds = (map['playerIds'] as List? ?? []).cast(); final DateTime? endedAt = map['endedAt'] != null ? DateTime.parse(map['endedAt'] as String) : null; final game = gameById[gameId]; final group = (groupId == null) ? null : groupById[groupId]; final players = playerIds .map((id) => playerById[id]) .whereType() .toList(); return Match( id: map['id'] as String, name: map['name'] as String, game: game ?? Game(name: 'Unknown', ruleset: Ruleset.singleWinner, description: '', color: GameColor.blue, icon: ''), group: group, players: players.isNotEmpty ? players : null, createdAt: DateTime.parse(map['createdAt'] as String), endedAt: endedAt, notes: map['notes'] as String? ?? '', ); }).toList(); // Import all data into the database await db.playerDao.addPlayersAsList(players: importedPlayers); await db.gameDao.addGamesAsList(games: importedGames); await db.groupDao.addGroupsAsList(groups: importedGroups); await db.teamDao.addTeamsAsList(teams: importedTeams); await db.matchDao.addMatchAsList(matches: importedMatches); return ImportResult.success; } on FormatException catch (e, stack) { print('[importData] FormatException'); print('[importData] $e'); print(stack); return ImportResult.formatException; } on Exception catch (e, stack) { print('[importData] Exception'); print('[importData] $e'); print(stack); return ImportResult.unknownException; } } /// Helper method to read file content from either bytes or path static Future _readFileContent(PlatformFile file) async { if (file.bytes != null) return utf8.decode(file.bytes!); if (file.path != null) return await File(file.path!).readAsString(); return null; } /// Validates the given JSON string against the predefined schema. static Future _validateJsonSchema(String jsonString) async { final String schemaString; schemaString = await rootBundle.loadString('assets/schema.json'); try { final schema = JsonSchema.create(json.decode(schemaString)); final jsonData = json.decode(jsonString); final result = schema.validate(jsonData); if (result.isValid) { return true; } return false; } catch (e, stack) { print('[validateJsonSchema] $e'); print(stack); return false; } } }