import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:flux/core/enums_and_consts/consts.dart'; import 'package:flux/features/attachments/models/attachment_model.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; enum Bucket { documents('documents'), companyDocuments('company_documents'); final String value; const Bucket(this.value); } class AttachmentsRepository { final _supabase = Supabase.instance.client; static const String _tableName = Tables.attachments; /// Scarica i byte di un file direttamente da Supabase Storage Future downloadAttachmentBytes({ required String storagePath, required Bucket bucket, }) async { try { final Uint8List bytes = await _supabase.storage .from(bucket.value) .download(storagePath); return bytes; } catch (e) { throw Exception("Impossibile scaricare il documento dal cloud: $e"); } } /// RESTITUISCE IL NOME DELLA COLONNA DB IN BASE AL TIPO String _getColumnNameForParent(AttachmentParentType parentType) { switch (parentType) { case AttachmentParentType.operation: return 'operation_id'; case AttachmentParentType.ticket: return 'ticket_id'; case AttachmentParentType.customer: return 'customer_id'; case AttachmentParentType.shippingDocument: return 'shipping_document_id'; case AttachmentParentType.note: return 'note_id'; } } /// RECUPERA I FILE IN TEMPO REALE Stream> getFilesStream( String parentId, AttachmentParentType parentType, ) { final columnName = _getColumnNameForParent(parentType); return _supabase .from(_tableName) .stream(primaryKey: ['id']) .eq(columnName, parentId) .map( (listOfMaps) => listOfMaps.map((map) => AttachmentModel.fromMap(map)).toList(), ); } /// CARICA IL FILE NELLO STORAGE E LO REGISTRA NEL DB Future uploadAndRegisterFile({ required String parentId, required AttachmentParentType parentType, required String companyId, required Bucket bucket, PlatformFile? pickedFile, // Ora è opzionale Uint8List? rawBytes, // Alternativa: bytes grezzi String? rawFileName, // Alternativa: nome del file }) async { // 🛡️ L'ASSERT NINJA: O c'è il pickedFile, o ci sono i byte e il nome. // L'assert funziona solo in debug, ma è perfetto per beccare subito errori di chiamata! assert( pickedFile != null || (rawBytes != null && rawFileName != null), 'Devi passare o un PlatformFile, oppure rawBytes e rawFileName!', ); try { // 1. Normalizziamo i dati in base a cosa ci è stato passato final Uint8List finalBytes; final String finalFileName; final int finalFileSize; if (pickedFile != null) { if (pickedFile.bytes == null) { throw Exception( "I bytes del file sono vuoti! Ricarica la pagina senza cache.", ); } finalBytes = pickedFile.bytes!; finalFileName = pickedFile.name; finalFileSize = pickedFile.size; } else { // Se pickedFile è null, grazie all'assert sappiamo che questi non lo sono finalBytes = rawBytes!; finalFileName = rawFileName!; finalFileSize = finalBytes.length; // Calcoliamo la size dai byte reali } // 2. Estraiamo l'estensione e puliamo il nome final extension = finalFileName.contains('.') ? finalFileName.split('.').last : ''; // Fallback se il file non ha estensione final cleanName = finalFileName .replaceAll(RegExp(r'[^\w\s\.-]'), '') .replaceAll(' ', '_'); // 3. Creiamo un path ordinato: idAzienda/tipoEntita/idEntita/timestamp_nomefile final timestamp = DateTime.now().millisecondsSinceEpoch; final storagePath = '$companyId/${parentType.name}/$parentId/${timestamp}_$cleanName'; // 4. Upload su Supabase Storage await _supabase.storage .from(bucket.value) .uploadBinary( storagePath, finalBytes, fileOptions: FileOptions(contentType: _guessContentType(extension)), ); // 5. Creiamo la mappa per il DB dinamicamente final Map insertData = { 'company_id': companyId, 'name': finalFileName.replaceAll('.$extension', ''), 'extension': extension, 'file_size': finalFileSize, 'storage_path': storagePath, }; // Inseriamo l'ID nella colonna giusta! final columnName = _getColumnNameForParent(parentType); insertData[columnName] = parentId; // 6. Salviamo su Postgres await _supabase.from(_tableName).insert(insertData); } catch (e) { throw Exception("Errore caricamento: $e"); } } /// ELIMINA IL FILE (Scollegamento intelligente) Future deleteFiles({ required List files, required AttachmentParentType currentContextType, required Bucket bucket, }) async { if (files.isEmpty) return; try { for (var file in files) { if (file.id == null) continue; // 1. Capiamo quali collegamenti ha questo file attualmente final currentLinks = { AttachmentParentType.operation: file.operationId, AttachmentParentType.ticket: file.ticketId, AttachmentParentType.customer: file.customerId, AttachmentParentType.shippingDocument: file.shippingDocumentId, }; // 2. Simuliamo la rimozione del collegamento per il contesto attuale currentLinks[currentContextType] = null; // 3. Controlliamo se rimangono altri ID valorizzati final hasOtherActiveLinks = currentLinks.values.any( (id) => id != null && id.isNotEmpty, ); if (hasOtherActiveLinks) { // A. Ci sono ancora altre entità che usano questo file! // Scolleghiamolo SOLO dal contesto attuale mettendo a NULL la sua colonna await _supabase .from(_tableName) .update({currentContextType.dbColumn: null}) .eq('id', file.id!); } else { // B. Nessuno usa più questo file! ELIMINAZIONE FISICA TOTALE. await _supabase.from(_tableName).delete().eq('id', file.id!); if (file.storagePath != null) { await _supabase.storage.from(bucket.value).remove([ file.storagePath!, ]); } } } } catch (e) { throw Exception("Errore nell'eliminazione dei file: $e"); } } /// RINOMINA UN FILE (Solo nel DB, non cambiamo il file fisico) Future renameAttachment(String fileId, String newName) async { try { await _supabase .from(_tableName) .update({'name': newName}) .eq('id', fileId); } catch (e) { throw Exception("Errore nella rinomina del file: $e"); } } /// ASSOCIA UN FILE A UN'ALTRA ENTITÀ (Modifica il record esistente) Future linkFileToEntity({ required String fileId, required AttachmentParentType targetType, required String targetId, }) async { try { // Facciamo un semplice UPDATE aggiungendo l'ID nella colonna giusta await _supabase .from(_tableName) .update({targetType.dbColumn: targetId}) .eq('id', fileId); } catch (e) { throw Exception("Errore nel collegamento del file: $e"); } } // Helper per indovinare il content-type base String _guessContentType(String extension) { switch (extension.toLowerCase()) { case 'pdf': return 'application/pdf'; case 'png': return 'image/png'; case 'jpg': case 'jpeg': return 'image/jpeg'; default: return 'application/octet-stream'; } } }