import 'dart:io'; import 'package:http/http.dart' as http; import '../config/intra_config.dart'; import 'intra_api_exception.dart'; import 'intra_auth_service.dart'; import 'intra_http.dart'; /// Compétence Intra : niveau entier + pourcentage (partie décimale du level API). class IntraSkillSummary { const IntraSkillSummary({ required this.name, required this.level, required this.percentage, }); final String name; final int level; final int percentage; } /// Projet terminé ou en échec. class IntraProjectSummary { const IntraProjectSummary({ required this.name, required this.status, this.finalMark, }); final String name; final String status; final int? finalMark; } /// Données agrégées pour la vue profil. class IntraUserProfile { const IntraUserProfile({ required this.user, required this.cursusLevel, required this.skills, required this.projects, }); final Map user; final double? cursusLevel; final List skills; final List projects; } class _ProfileCacheEntry { _ProfileCacheEntry(this.profile, this.fetchedAt); final IntraUserProfile profile; final DateTime fetchedAt; } /// Agrège fiche user, skills (cursus, pas `/experiences`) et `projects_users` (scope `projects`). class IntraProfileService { IntraProfileService._(); static final IntraProfileService instance = IntraProfileService._(); /// Cache court + délai entre GET pour limiter les 429 sur l'API 42. static const Duration _cacheTtl = Duration(minutes: 2); static const Duration _betweenRequests = Duration(milliseconds: 450); final IntraAuthService _auth = IntraAuthService.instance; final Map _cache = {}; /// Charge profil complet (user, cursus, skills, projets) avec le token existant. Future fetchProfile(Map summaryUser) async { if (!_auth.isAuthenticated) { throw const IntraNotAuthenticatedException(); } final userId = summaryUser['id']; if (userId == null) { throw const IntraApiErrorException('Identifiant utilisateur manquant'); } final id = userId.toString(); final cached = _cache[id]; if (cached != null && DateTime.now().difference(cached.fetchedAt) < _cacheTtl) { return cached.profile; } try { final login = summaryUser['login']?.toString(); final userPath = login != null && login.isNotEmpty ? '/users/$login' : '/users/$id'; final userDecoded = await _getJson(userPath); if (userDecoded is! Map) { throw IntraApiErrorException('Réponse API inattendue ($userPath)'); } final embeddedCursus = userDecoded['cursus_users']; final List cursusList; if (embeddedCursus is List && embeddedCursus.isNotEmpty) { cursusList = embeddedCursus; } else { await Future.delayed(_betweenRequests); cursusList = await _getJsonList('/users/$id/cursus_users'); } await Future.delayed(_betweenRequests); final projectsUsers = await _getJsonList( '/users/$id/projects_users', query: {'page[size]': '100'}, ); final primaryCursus = _pickPrimaryCursus(cursusList); final profile = IntraUserProfile( user: userDecoded, cursusLevel: _levelFromCursus(primaryCursus), skills: _parseSkillsFromCursus(primaryCursus, cursusList), projects: _parseProjects(projectsUsers), ); _cache[id] = _ProfileCacheEntry(profile, DateTime.now()); return profile; } on IntraApiException { rethrow; } on SocketException { throw const IntraNetworkException(); } on http.ClientException { throw const IntraNetworkException(); } } Future _getJson(String path, {Map? query}) async { final uri = Uri.parse('${IntraConfig.apiBaseUrl}$path').replace( queryParameters: query, ); final response = await intraApiGet(uri, auth: _auth); return decodeIntraBody(response); } Future> _getJsonList( String path, { Map? query, }) async { final decoded = await _getJson(path, query: query); if (decoded is! List) { throw IntraApiErrorException('Réponse API inattendue ($path)'); } return decoded; } Map? _pickPrimaryCursus(List cursusUsers) { Map? fallback; for (final entry in cursusUsers) { if (entry is! Map) continue; final cursus = entry['cursus']; final slug = cursus is Map ? cursus['slug']?.toString() : null; final name = cursus is Map ? cursus['name']?.toString() : null; if (slug == '42' || name == '42' || name == '42cursus') { return entry; } fallback ??= entry; } return fallback; } double? _levelFromCursus(Map? cursusUser) { if (cursusUser == null) return null; final level = cursusUser['level']; return level is num ? level.toDouble() : null; } List _parseSkillsFromCursus( Map? primaryCursus, List allCursus, ) { final sources = >[]; if (primaryCursus != null) { sources.add(primaryCursus); } else { for (final c in allCursus) { if (c is Map) sources.add(c); } } final byId = {}; for (final cursusUser in sources) { final skills = cursusUser['skills']; if (skills is! List) continue; for (final skill in skills) { if (skill is! Map) continue; final idRaw = skill['id']; if (idRaw is! num) continue; final skillId = idRaw.toInt(); final name = skill['name']?.toString() ?? 'Skill $skillId'; final levelRaw = skill['level']; if (levelRaw is! num) continue; final summary = _skillFromLevel(name, levelRaw.toDouble()); final existing = byId[skillId]; if (existing == null || summary.level > existing.level) { byId[skillId] = summary; } } } final list = byId.values.toList(); list.sort((a, b) => b.level.compareTo(a.level)); return list; } IntraSkillSummary _skillFromLevel(String name, double levelValue) { final whole = levelValue.floor(); final fraction = levelValue - whole; final percentage = (fraction * 100).round().clamp(0, 99); return IntraSkillSummary( name: name, level: whole, percentage: percentage, ); } List _parseProjects(List projectsUsers) { final projects = []; for (final entry in projectsUsers) { if (entry is! Map) continue; final status = entry['status']?.toString(); if (status != 'finished' && status != 'failed') continue; final project = entry['project']; final name = project is Map ? (project['name']?.toString() ?? project['slug']?.toString()) : null; if (name == null || name.isEmpty) continue; final mark = entry['final_mark']; projects.add( IntraProjectSummary( name: name, status: status!, finalMark: mark is int ? mark : (mark is num ? mark.round() : null), ), ); } projects.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); return projects; } }