import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miaoji_zhang/shared/api/api_client.dart'; import 'package:miaoji_zhang/shared/api/backend_identity.dart'; import 'package:miaoji_zhang/shared/services/current_ledger_store.dart'; import 'package:miaoji_zhang/shared/services/local_export_service.dart'; import 'package:miaoji_zhang/shared/services/session_store.dart'; import 'package:miaoji_zhang/shared/services/push_service.dart'; import 'package:miaoji_zhang/shared/services/shanghai_time.dart'; class AiCompanion { final String avatarKey; final String personaKey; final String? customName; final int roastLevel, stickerFrequency, proactiveLevel; AiCompanion.fromJson(Map json) : avatarKey = json['avatarKey'] as String, personaKey = json['personaKey'] as String, customName = json['customName'] as String?, roastLevel = json['roastLevel'] as int, stickerFrequency = json['stickerFrequency'] as int, proactiveLevel = json['proactiveLevel'] as int; Map toJson() => { 'avatarKey': avatarKey, 'personaKey': personaKey, 'customName': customName, 'roastLevel': roastLevel, 'stickerFrequency': stickerFrequency, 'proactiveLevel': proactiveLevel, }; } class UserProfile { final int userId; final String username; final String? nickname; final String appMode; final AiCompanion? aiCompanion; final bool onboardingDone; final bool aiEnabled; UserProfile.fromJson(Map json) : userId = (json['userId'] as num).toInt(), username = json['username'] as String, nickname = json['nickname'] as String?, appMode = json['appMode'] as String, aiCompanion = json['aiCompanion'] == null ? null : AiCompanion.fromJson(json['aiCompanion'] as Map), onboardingDone = json['onboardingDone'] as bool, aiEnabled = ((json['permissions'] as Map?)?['ai'] as bool?) ?? true; const UserProfile.local({ required this.userId, required this.username, required this.nickname, required this.appMode, required this.onboardingDone, this.aiCompanion, this.aiEnabled = false, }); } class AuthLoginResult { final bool accountClosureCancelled; final UserProfile profile; const AuthLoginResult({ required this.accountClosureCancelled, required this.profile, }); } class AuthApi { static final _dio = ApiClient.instance.dio; static Future register( String username, String password, { required bool agreedToTerms, }) async { final response = await _dio.post( '/api/auth/register', data: { 'username': username, 'password': password, 'agreedToTerms': agreedToTerms, }, ); await ApiClient.instance.saveToken(response.data['token'] as String); return me(forceRemote: true); } static Future login(String username, String password) async { final response = await _dio.post( '/api/auth/login', data: {'username': username, 'password': password}, ); await ApiClient.instance.saveToken(response.data['token'] as String); final profile = await me(forceRemote: true); return AuthLoginResult( accountClosureCancelled: response.data['accountClosureCancelled'] as bool? ?? false, profile: profile, ); } static Future me({bool forceRemote = false}) async { final session = SessionStore.instance; if (!forceRemote && (session.isGuest || (session.isAccount && session.shouldUseLocalOnly))) { return _localProfile(); } try { final response = await _dio.get('/api/users/me'); final profile = UserProfile.fromJson( response.data as Map, ); await _cacheProfile(profile); return profile; } catch (error) { if (session.isAccount && (isConnectivityError(error) || session.needsReauth)) { return _localProfile(); } rethrow; } } static Future cachedCompanion() async { final userId = SessionStore.instance.userId; if (userId == null) return null; final value = (await SharedPreferences.getInstance()).getString( _companionCacheKey(userId), ); if (value == null) return null; try { return AiCompanion.fromJson(jsonDecode(value) as Map); } catch (_) { return null; } } static Future completeOnboarding({ required String appMode, required String avatarKey, required String personaKey, String? customName, }) async { SessionStore.instance.requireOnline('完成账号初始化需要连接网络'); final response = await _dio.post( '/api/users/me/onboarding', data: { 'appMode': appMode, 'avatarKey': avatarKey, 'personaKey': personaKey, 'customName': customName, }, ); final profile = UserProfile.fromJson(response.data as Map); await _cacheProfile(profile); return profile; } static Future switchMode(String appMode) async { SessionStore.instance.requireOnline('切换 AI 模式需要登录并连接网络'); final response = await _dio.put( '/api/users/me/mode', data: {'appMode': appMode}, ); final profile = UserProfile.fromJson(response.data as Map); await _cacheProfile(profile); return profile; } static Future updateCompanion({ String? avatarKey, String? personaKey, String? customName, int? roastLevel, int? stickerFrequency, int? proactiveLevel, }) async { SessionStore.instance.requireOnline('AI 伙伴设置需要登录并连接网络'); final response = await _dio.put( '/api/users/me/companion', data: { 'avatarKey': avatarKey, 'personaKey': personaKey, 'customName': customName, 'roastLevel': roastLevel, 'stickerFrequency': stickerFrequency, 'proactiveLevel': proactiveLevel, }, ); final profile = UserProfile.fromJson(response.data as Map); await _cacheProfile(profile); return profile; } static Future updateProfile(String nickname) async { final session = SessionStore.instance; final normalized = nickname.trim(); if (session.isGuest) { await session.updateNickname(normalized); return _localProfile(); } session.requireOnline('修改云端资料需要连接网络'); final previous = session.nickname; await session.updateNickname(normalized); try { final response = await _dio.put( '/api/users/me/profile', data: {'nickname': normalized}, ); final profile = UserProfile.fromJson( response.data as Map, ); await _cacheProfile(profile); return profile; } catch (_) { await session.updateNickname(previous); rethrow; } } static Future changePassword( String currentPassword, String newPassword, ) async { SessionStore.instance.requireOnline('修改密码需要连接网络'); final response = await _dio.put( '/api/users/me/password', data: {'currentPassword': currentPassword, 'newPassword': newPassword}, ); await ApiClient.instance.saveToken(response.data['token'] as String); } static Future> exportData() async { if (SessionStore.instance.shouldUseLocalOnly) { return LocalExportService.buildZip(); } try { final response = await _dio.get>( '/api/users/me/export', options: Options(responseType: ResponseType.bytes), ); return response.data ?? const []; } catch (error) { if (isConnectivityError(error)) return LocalExportService.buildZip(); rethrow; } } static Future requestAccountClosure( String password, String confirmationText, ) async { SessionStore.instance.requireOnline('注销账号需要连接网络'); final response = await _dio.post( '/api/users/me/closure', data: {'password': password, 'confirmationText': confirmationText}, ); final scheduledAt = ShanghaiTime.parseCivil( response.data['scheduledAt'] as String, ); await logout(); return scheduledAt; } static Future deleteAccount(String password) async { SessionStore.instance.requireOnline('注销账号需要连接网络'); await _dio.delete('/api/users/me', data: {'password': password}); await logout(); } static Future logout() async { CurrentLedgerStore.instance.clear(); await PushService.instance.logout(); await ApiClient.instance.clearToken(); await SessionStore.instance.clearActiveSession(); } static UserProfile _localProfile() { final session = SessionStore.instance; return UserProfile.local( userId: session.userId ?? 0, username: session.username ?? '游客', nickname: session.nickname, appMode: session.isGuest ? 'normal' : session.appMode, onboardingDone: session.isGuest || session.onboardingDone, aiEnabled: session.aiEnabled, ); } static Future _cacheProfile(UserProfile profile) async { await SessionStore.instance.activateAccount( userId: profile.userId, username: profile.username, nickname: profile.nickname, appMode: profile.appMode, onboardingDone: profile.onboardingDone, aiEnabled: profile.aiEnabled, ); final companion = profile.aiCompanion; if (companion != null) { await (await SharedPreferences.getInstance()).setString( _companionCacheKey(profile.userId), jsonEncode(companion.toJson()), ); } } static String _companionCacheKey(int userId) => 'ai_companion_${BackendIdentity.scope}_$userId'; }