import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'global_data.dart'; import 'cid_data_manager.dart'; import 'security_service.dart'; class ScambioDatiScreen extends StatefulWidget { const ScambioDatiScreen({super.key}); @override State createState() => _ScambioDatiScreenState(); } class _ScambioDatiScreenState extends State with SingleTickerProviderStateMixin { late TabController _tabController; final MobileScannerController _cameraController = MobileScannerController(); final TextEditingController _manualCodeController = TextEditingController(); String? _codiceSessione; String? _chiaveSegreta; String? _shortCode; // IL PIN DI 6 LETTERE StreamSubscription? _streamSubscription; bool _isLoading = false; bool _scambioConcluso = false; @override void initState() { super.initState(); // 🧹 AVVIO DEL NETTURBINO (con 10% di probabilità per non intasare il server) if (Random().nextInt(10) == 0) { _pulisciVecchieSessioni(); } // RIMOZIONE DELLA SECONDA CHIAMATA LIBERA // LOGICA DI APERTURA INTELLIGENTE: // Lato A -> Inizia con la Fotocamera (Indice 1) // Lato B -> Inizia mostrando il QR (Indice 0) int initialIndex = (GlobalData.latoCorrente == 'A') ? 1 : 0; _tabController = TabController(length: 2, vsync: this, initialIndex: initialIndex); // Avvio automatico Host (prepara il QR in background a prescindere dalla Tab aperta) _avviaHost(); } // --- 🧹 IL NETTURBINO (GARBAGE COLLECTOR) --- // Cerca nel database le sessioni più vecchie di 2 ore e le distrugge Future _pulisciVecchieSessioni() async { try { final dueOreFa = DateTime.now().subtract(const Duration(hours: 2)); final snapshot = await FirebaseFirestore.instance .collection('scambi_cid') .where('timestamp_scambio', isLessThan: Timestamp.fromDate(dueOreFa)) .get(); int cancellati = 0; for (var doc in snapshot.docs) { await doc.reference.delete(); cancellati++; } debugPrint("🧹 [NETTURBINO] Pulizia completata: eliminate $cancellati sessioni vecchie."); } catch (e) { debugPrint("⚠️ [NETTURBINO] Errore durante la pulizia: $e"); } } @override void dispose() { // Cancello la sessione se esco senza concludere, a meno che non sia l'ID della firma if (!_scambioConcluso && _codiceSessione != null && _codiceSessione != GlobalData.idSessione) { FirebaseFirestore.instance.collection('scambi_cid').doc(_codiceSessione).delete(); } _streamSubscription?.cancel(); _tabController.dispose(); _cameraController.dispose(); _manualCodeController.dispose(); super.dispose(); } // --- GENERATORE DI PIN A 6 LETTERE MAIUSCOLE --- String _generaShortCode() { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; Random rnd = Random(); return String.fromCharCodes(Iterable.generate(6, (_) => chars.codeUnitAt(rnd.nextInt(chars.length)))); } // --- 1. LOGICA HOST (CHI MOSTRA IL QR) --- Future _avviaHost() async { if (_codiceSessione != null) return; if ((GlobalData.latoCorrente == 'A' && GlobalData.puntiFirmaA.isEmpty) || (GlobalData.latoCorrente == 'B' && GlobalData.puntiFirmaB.isEmpty)) { return; } setState(() => _isLoading = true); try { String sessionId = GlobalData.idSessione ?? "CID_${DateTime.now().millisecondsSinceEpoch}"; String shortCode = _generaShortCode(); GlobalData.idSessione = sessionId; GlobalData.idScambioTemporaneo = sessionId; String chiave = GlobalData.chiaveSegretaCorrente ?? SecurityService.generaChiaveSessione(); GlobalData.chiaveSegretaCorrente = chiave; Map mieiDati = CidDataManager.estraiDatiPerExport(); String payloadCriptato = SecurityService.criptaDati(mieiDati, chiave); await FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).set({ "timestamp_scambio": FieldValue.serverTimestamp(), "host_lato": GlobalData.latoCorrente, "secure_payload_host": payloadCriptato, "secure_payload_guest": null, "status": "waiting_guest", "short_code": shortCode, "chiave_temporanea": chiave }, SetOptions(merge: true)); if (mounted) { setState(() { _codiceSessione = sessionId; _chiaveSegreta = chiave; _shortCode = shortCode; _isLoading = false; }); } // Ascolto risposta dell'altro telefono _streamSubscription = FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).snapshots().listen((snapshot) { if (!snapshot.exists) { if (mounted) setState(() => _codiceSessione = null); return; } var data = snapshot.data(); if (data != null && data['secure_payload_guest'] != null) { _streamSubscription?.cancel(); _completaSync(data['secure_payload_guest'], chiave); } }); } catch (e) { debugPrint("Err Host: $e"); if (mounted) setState(() => _isLoading = false); } } void _completaSync(String encryptedData, String chiave) { try { Map dati = SecurityService.decriptaDati(encryptedData, chiave); CidDataManager.importaDati(dati); _scambioConcluso = true; _showSuccessAndExit(); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Errore decriptazione"))); } } // --- 2. LOGICA GUEST: VIA FOTOCAMERA --- void _onDetect(BarcodeCapture capture) { if (_isLoading || _scambioConcluso) return; final List barcodes = capture.barcodes; for (final barcode in barcodes) { if (barcode.rawValue != null) { _partecipaGuestScansione(barcode.rawValue!); break; } } } Future _partecipaGuestScansione(String qrRawData) async { if (qrRawData.isEmpty) return; setState(() => _isLoading = true); try { if (!qrRawData.contains('|')) throw Exception("QR non valido"); var parts = qrRawData.split('|'); String sessionId = parts[0]; String chiave = parts[1]; await _eseguiPartecipazione(sessionId, chiave); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore Scansione: $e"))); await Future.delayed(const Duration(seconds: 2)); setState(() => _isLoading = false); } } } // --- 3. LOGICA GUEST: INSERIMENTO MANUALE --- Future _partecipaGuestManuale() async { String codiceInserito = _manualCodeController.text.trim().toUpperCase(); if (codiceInserito.length < 5) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Inserisci un codice valido!"), backgroundColor: Colors.orange)); return; } setState(() => _isLoading = true); try { // 1. Cerca la sessione tramite codice corto su Firebase final querySnapshot = await FirebaseFirestore.instance .collection('scambi_cid') .where('short_code', isEqualTo: codiceInserito) .limit(1) .get(); if (querySnapshot.docs.isEmpty) { throw Exception("Codice non trovato o scaduto."); } final doc = querySnapshot.docs.first; String sessionId = doc.id; String chiave = doc['chiave_temporanea'] ?? "CHIAVE_DI_BACKUP"; await _eseguiPartecipazione(sessionId, chiave, manualHostData: doc.data()); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore: $e"), backgroundColor: Colors.red)); setState(() => _isLoading = false); } } } // --- FUNZIONE CENTRALE PER GUEST (USATA DA FOTOCAMERA E MANUALE) --- Future _eseguiPartecipazione(String sessionId, String chiave, {Map? manualHostData}) async { if (GlobalData.idSessione != null && GlobalData.idSessione != sessionId) { debugPrint("🗑️ Cancello vecchio file firma orfano: ${GlobalData.idSessione}"); await FirebaseFirestore.instance.collection('scambi_cid').doc(GlobalData.idSessione).delete(); GlobalData.idSessione = null; } Map data; if (manualHostData != null) { data = manualHostData; } else { DocumentSnapshot doc = await FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).get(); if (!doc.exists) throw Exception("Sessione scaduta."); data = doc.data() as Map; } String? hostDataEnc = data['secure_payload_host']; if (hostDataEnc == null) throw Exception("Dati host mancanti."); Map datiHost = SecurityService.decriptaDati(hostDataEnc, chiave); CidDataManager.importaDati(datiHost); Map mieiDati = CidDataManager.estraiDatiPerExport(); String myDataEnc = SecurityService.criptaDati(mieiDati, chiave); await FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).update({ "secure_payload_guest": myDataEnc, "status": "completed" }); GlobalData.idSessione = sessionId; GlobalData.idScambioTemporaneo = sessionId; GlobalData.chiaveSegretaCorrente = chiave; _scambioConcluso = true; _showSuccessAndExit(); } void _showSuccessAndExit() { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("SCAMBIO EFFETTUATO!"), backgroundColor: Colors.green, duration: Duration(seconds: 1)) ); Navigator.pop(context); } @override Widget build(BuildContext context) { String qrString = ""; if (_codiceSessione != null && _chiaveSegreta != null) { qrString = "$_codiceSessione|$_chiaveSegreta"; } return Scaffold( backgroundColor: Colors.white, appBar: AppBar( title: const Text("Scambio Dati"), backgroundColor: Colors.blue.shade900, foregroundColor: Colors.white, bottom: TabBar( controller: _tabController, indicatorColor: Colors.white, labelColor: Colors.white, unselectedLabelColor: Colors.white70, tabs: const [ Tab(icon: Icon(Icons.qr_code), text: "IL TUO QR"), Tab(icon: Icon(Icons.camera_alt), text: "SCANSIONA / CODICE") ] ), ), body: TabBarView( controller: _tabController, // Impedisce di strusciare accidentalmente e chiudere lo scanner physics: const NeverScrollableScrollPhysics(), children: [ // ================= TAB 1: HOST (MOSTRA QR E PIN) ================= Center( child: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (_codiceSessione == null) ...[ const CircularProgressIndicator(), const SizedBox(height: 20), const Text("Generazione in corso...") ] else ...[ const Text("Fai scansionare questo QR:", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), const SizedBox(height: 20), Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration(color: Colors.white, border: Border.all(color: Colors.black12)), child: QrImageView(data: qrString, size: 240) ), const SizedBox(height: 30), const Text("Oppure fai inserire questo PIN:", style: TextStyle(color: Colors.black54)), const SizedBox(height: 5), Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.orange.shade800, width: 2) ), child: Text( _shortCode ?? "---", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 32, letterSpacing: 6, color: Colors.orange.shade900) ), ), const SizedBox(height: 30), const CircularProgressIndicator(), const SizedBox(height: 10), const Text("In attesa dell'altro utente...", style: TextStyle(color: Colors.grey)) ] ] ) ) ), // ================= TAB 2: GUEST (SCANNER SPLIT SCREEN) ================= Stack( children: [ Column( children: [ // METÀ SUPERIORE: FOTOCAMERA Expanded( flex: 5, child: Stack( children: [ MobileScanner( controller: _cameraController, onDetect: _onDetect, ), // Overlay mirino per far capire all'utente dove inquadrare Center( child: Container( width: 200, height: 200, decoration: BoxDecoration( border: Border.all(color: Colors.greenAccent, width: 3), borderRadius: BorderRadius.circular(20) ), ), ), Positioned( bottom: 10, left: 0, right: 0, child: Container( color: Colors.black54, padding: const EdgeInsets.symmetric(vertical: 8), child: const Text( "Inquadra il QR Code", textAlign: TextAlign.center, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ) ) ], ), ), // DIVISORIO Container(height: 5, color: Colors.blue.shade900), // METÀ INFERIORE: INSERIMENTO MANUALE Expanded( flex: 5, child: Container( color: Colors.grey.shade100, width: double.infinity, child: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.keyboard, size: 40, color: Colors.blueGrey), const SizedBox(height: 10), const Text( "La fotocamera non funziona?\nInserisci qui il PIN a 6 lettere dell'altro utente:", textAlign: TextAlign.center, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87), ), const SizedBox(height: 20), TextField( controller: _manualCodeController, textCapitalization: TextCapitalization.characters, maxLength: 6, textAlign: TextAlign.center, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24, letterSpacing: 8), decoration: InputDecoration( hintText: "PIN", counterText: "", border: OutlineInputBorder(borderRadius: BorderRadius.circular(15)), filled: true, fillColor: Colors.white, prefixIcon: const Icon(Icons.password, color: Colors.blueGrey), ), ), const SizedBox(height: 20), SizedBox( width: double.infinity, child: ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: Colors.orange.shade800, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), ), onPressed: _partecipaGuestManuale, icon: const Icon(Icons.download), label: const Text("SCARICA DATI", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), ), ) ], ), ), ), ) ], ), // OVERLAY CARICAMENTO if (_isLoading) const ColoredBox(color: Colors.black54, child: Center(child: CircularProgressIndicator(color: Colors.white))), ], ) ], ), ); } } // Test di sincronizzazione forzata ore 22:40