import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/server_config.dart'; import '../providers/providers.dart'; import '../services/mqtt_service.dart' show ConnectionStatus; import '../services/purchase_service.dart'; import '../services/wol_service.dart'; import '../theme/app_theme.dart'; import '../widgets/status_dot.dart'; import 'trace_screen.dart'; class SettingsScreen extends ConsumerStatefulWidget { const SettingsScreen({super.key}); @override ConsumerState createState() => _SettingsScreenState(); } class _SettingsScreenState extends ConsumerState { final _formKey = GlobalKey(); late final TextEditingController _localHostController; late final TextEditingController _vpnHostController; late final TextEditingController _remoteHostController; late final TextEditingController _portController; late final TextEditingController _macController; late final TextEditingController _mqttTokenController; bool _isWaking = false; bool _iapLoading = false; @override void initState() { super.initState(); PurchaseService.instance.addListener(_onPurchaseChanged); final config = ref.read(serverConfigProvider); _localHostController = TextEditingController(text: config?.localHost ?? ''); _vpnHostController = TextEditingController(text: config?.vpnHost ?? ''); _remoteHostController = TextEditingController(text: config?.host ?? ''); _portController = TextEditingController(text: '${config?.port ?? 8765}'); _macController = TextEditingController(text: config?.macAddress ?? ''); _mqttTokenController = TextEditingController(text: config?.mqttToken ?? ''); } @override void dispose() { PurchaseService.instance.removeListener(_onPurchaseChanged); _localHostController.dispose(); _vpnHostController.dispose(); _remoteHostController.dispose(); _portController.dispose(); _macController.dispose(); _mqttTokenController.dispose(); super.dispose(); } void _onPurchaseChanged() { if (!mounted) return; final isPro = PurchaseService.instance.isPro; ref.read(isProProvider.notifier).state = isPro; setState(() => _iapLoading = PurchaseService.instance.isLoading); final error = PurchaseService.instance.errorMessage; if (error != null && mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(error)), ); } if (isPro && mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('PAILot Pro activated. Enjoy unlimited sessions!'), duration: Duration(seconds: 3), ), ); } } Future _handleUpgrade() async { setState(() => _iapLoading = true); await PurchaseService.instance.purchaseFullAccess(); } Future _handleRestore() async { setState(() => _iapLoading = true); await PurchaseService.instance.restorePurchases(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Checking for previous purchases...'), duration: Duration(seconds: 2), ), ); } } Future _save() async { if (!_formKey.currentState!.validate()) return; final config = ServerConfig( host: _remoteHostController.text.trim(), port: int.tryParse(_portController.text.trim()) ?? 8765, localHost: _localHostController.text.trim().isEmpty ? null : _localHostController.text.trim(), vpnHost: _vpnHostController.text.trim().isEmpty ? null : _vpnHostController.text.trim(), macAddress: _macController.text.trim().isEmpty ? null : _macController.text.trim(), mqttToken: _mqttTokenController.text.trim().isEmpty ? null : _mqttTokenController.text.trim(), ); await ref.read(serverConfigProvider.notifier).save(config); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Server configuration saved'), duration: Duration(seconds: 2), ), ); } } Future _wakeMac() async { final mac = _macController.text.trim(); if (mac.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('No MAC address configured')), ); return; } setState(() => _isWaking = true); try { await WolService.wake(mac, localHost: _localHostController.text.trim()); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Wake-on-LAN packet sent')), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('WoL failed: $e')), ); } } finally { if (mounted) setState(() => _isWaking = false); } } @override Widget build(BuildContext context) { final wsStatus = ref.watch(wsStatusProvider); return Scaffold( appBar: AppBar( title: const Text('Settings'), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Connection status card Card( child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ StatusDot(status: wsStatus), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _statusText(wsStatus), style: Theme.of(context).textTheme.bodyLarge, ), if (ref.watch(connectedViaProvider).isNotEmpty) Text( 'via ${ref.watch(connectedViaProvider)}', style: TextStyle( fontSize: 12, color: Colors.grey.shade500, ), ), ], ), ), ], ), ), ), const SizedBox(height: 24), // Local address Text('Local Address', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 4), TextFormField( controller: _localHostController, decoration: const InputDecoration( hintText: '192.168.1.100', ), keyboardType: TextInputType.url, validator: (v) { if (v == null || v.trim().isEmpty) return null; final ip = RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'); return ip.hasMatch(v.trim()) ? null : 'Enter a valid IP address'; }, ), const SizedBox(height: 16), // VPN address Text('VPN Address', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 4), TextFormField( controller: _vpnHostController, decoration: const InputDecoration( hintText: '10.8.0.1 (OpenVPN static IP)', ), keyboardType: TextInputType.url, validator: (v) { if (v == null || v.trim().isEmpty) return null; final ip = RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'); return ip.hasMatch(v.trim()) ? null : 'Enter a valid IP address'; }, ), const SizedBox(height: 16), // Remote address Text('Remote Address', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 4), TextFormField( controller: _remoteHostController, decoration: const InputDecoration( hintText: 'your-server.example.com', ), keyboardType: TextInputType.url, validator: (v) => (v == null || v.trim().isEmpty) ? 'Required' : null, ), const SizedBox(height: 16), // Port Text('Port', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 4), TextFormField( controller: _portController, decoration: const InputDecoration( hintText: '8765', ), keyboardType: TextInputType.number, validator: (v) { if (v == null || v.trim().isEmpty) return 'Required'; final port = int.tryParse(v.trim()); if (port == null || port < 1 || port > 65535) { return 'Port must be 1–65535'; } return null; }, ), const SizedBox(height: 16), // MAC address Text('MAC Address (for Wake-on-LAN)', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 4), TextFormField( controller: _macController, decoration: const InputDecoration( hintText: 'AA:BB:CC:DD:EE:FF', ), validator: (v) { if (v == null || v.trim().isEmpty) return null; final mac = RegExp( r'^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$'); return mac.hasMatch(v.trim()) ? null : 'Enter a valid MAC address (AA:BB:CC:DD:EE:FF)'; }, ), const SizedBox(height: 16), // MQTT Token Text('MQTT Token', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 4), TextFormField( controller: _mqttTokenController, decoration: const InputDecoration( hintText: 'Shared secret for MQTT auth', ), obscureText: true, ), const SizedBox(height: 24), // Save button ElevatedButton( onPressed: _save, style: ElevatedButton.styleFrom( backgroundColor: AppColors.accent, foregroundColor: Colors.white, ), child: const Text('Save Configuration'), ), const SizedBox(height: 12), // Wake Mac button OutlinedButton.icon( onPressed: _isWaking ? null : _wakeMac, icon: _isWaking ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.power_settings_new), label: const Text('Wake Mac'), ), const SizedBox(height: 12), // Reset TLS Trust button OutlinedButton.icon( onPressed: () async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Reset Server Trust?'), content: const Text( 'This clears the saved server certificate fingerprint. ' 'Use this if you reinstalled AIBroker or changed servers. ' 'The app will trust the next server it connects to.', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel'), ), TextButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Reset', style: TextStyle(color: AppColors.error)), ), ], ), ); if (confirmed == true && mounted) { // Access MqttService through the provider and reset trust final prefs = await SharedPreferences.getInstance(); await prefs.remove('trustedCertFingerprint'); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Server trust reset. Reconnect to trust the new server.')), ); } }, icon: const Icon(Icons.shield_outlined), label: const Text('Reset Server Trust'), ), const SizedBox(height: 12), // Message Trace Log — for diagnosing message delivery problems OutlinedButton.icon( onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => const TraceScreen(), ), ); }, icon: const Icon(Icons.receipt_long_outlined), label: const Text('Message Trace Log'), ), const SizedBox(height: 24), // --- PAILot Pro --- const Divider(), const SizedBox(height: 8), Row( children: [ const Icon(Icons.star, size: 18), const SizedBox(width: 8), Text( 'PAILot Pro', style: Theme.of(context).textTheme.titleMedium, ), const Spacer(), Consumer( builder: (ctx, ref, _) { final isPro = ref.watch(isProProvider); return Chip( label: Text( isPro ? 'Active' : 'Free Tier', style: TextStyle( fontSize: 11, color: isPro ? Colors.white : null, ), ), backgroundColor: isPro ? AppColors.accent : null, padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, ); }, ), ], ), const SizedBox(height: 4), Text( 'Free: 2 sessions, messages expire after 15 min\n' 'Pro: unlimited sessions, messages persist forever', style: Theme.of(context) .textTheme .bodySmall ?.copyWith(color: Colors.grey.shade500), ), const SizedBox(height: 12), Consumer( builder: (ctx, ref, _) { final isPro = ref.watch(isProProvider); if (isPro) { return const Center( child: Text( 'Full access active', style: TextStyle(color: AppColors.accent), ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ElevatedButton.icon( onPressed: _iapLoading ? null : _handleUpgrade, icon: _iapLoading ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.upgrade), label: const Text('Upgrade to Pro — \$4.99'), style: ElevatedButton.styleFrom( backgroundColor: AppColors.accent, foregroundColor: Colors.white, ), ), const SizedBox(height: 8), OutlinedButton.icon( onPressed: _iapLoading ? null : _handleRestore, icon: const Icon(Icons.restore), label: const Text('Restore Purchase'), ), ], ); }, ), const SizedBox(height: 12), ], ), ), ), ); } String _statusText(ConnectionStatus status) { switch (status) { case ConnectionStatus.connected: return 'Connected'; case ConnectionStatus.connecting: return 'Connecting...'; case ConnectionStatus.reconnecting: return 'Reconnecting...'; case ConnectionStatus.disconnected: return 'Disconnected'; } } }