import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:pluto_grid/pluto_grid.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:path/path.dart' as p; import 'package:nethive_neo/helpers/globals.dart'; import 'package:nethive_neo/models/media/media_models.dart'; class VideosProvider extends ChangeNotifier { // ========== ORGANIZATION CONSTANT ========== static const int organizationId = 17; // ========== STATE MANAGEMENT ========== PlutoGridStateManager? stateManager; List videosRows = []; // ========== DATA LISTS ========== List mediaFiles = []; List categories = []; List mediaWithPosters = []; // ========== CONTROLLERS ========== final busquedaVideoController = TextEditingController(); final tituloController = TextEditingController(); final descripcionController = TextEditingController(); // ========== VIDEO/IMAGE UPLOAD STATE ========== String? videoName; String? videoUrl; String? videoStoragePath; String videoFileExtension = ''; Uint8List? webVideoBytes; String? posterName; String? posterUrl; String? posterStoragePath; String posterFileExtension = ''; Uint8List? webPosterBytes; // ========== LOADING STATE ========== bool isLoading = false; String? errorMessage; // ========== CONSTRUCTOR ========== VideosProvider() { loadMediaFiles(); loadCategories(); } // ========== LOAD METHODS ========== /// Load all media files with organization filter Future loadMediaFiles() async { try { isLoading = true; errorMessage = null; notifyListeners(); final response = await supabaseML .from('media_files') .select() .eq('organization_fk', organizationId) .order('created_at_timestamp', ascending: false); mediaFiles = (response as List) .map((item) => MediaFileModel.fromMap(item)) .toList(); await _buildPlutoRows(); isLoading = false; notifyListeners(); } catch (e) { errorMessage = 'Error cargando videos: $e'; isLoading = false; notifyListeners(); print('Error en loadMediaFiles: $e'); } } /// Load media files with posters using view Future loadMediaWithPosters() async { try { isLoading = true; notifyListeners(); final response = await supabaseML .from('vw_media_files_with_posters') .select() .eq('organization_fk', organizationId) .order('media_created_at', ascending: false); mediaWithPosters = (response as List) .map((item) => MediaWithPosterModel.fromMap(item)) .toList(); isLoading = false; notifyListeners(); } catch (e) { errorMessage = 'Error cargando videos con posters: $e'; isLoading = false; notifyListeners(); print('Error en loadMediaWithPosters: $e'); } } /// Load all categories Future loadCategories() async { try { final response = await supabaseML .from('media_categories') .select() .order('category_name'); categories = (response as List) .map((item) => MediaCategoryModel.fromMap(item)) .toList(); notifyListeners(); } catch (e) { print('Error en loadCategories: $e'); } } /// Build PlutoGrid rows from media files Future _buildPlutoRows() async { videosRows.clear(); for (var media in mediaFiles) { videosRows.add( PlutoRow( cells: { 'id': PlutoCell(value: media.mediaFileId), 'thumbnail': PlutoCell(value: media.fileUrl), // Para mostrar thumbnail 'title': PlutoCell(value: media.title ?? media.fileName), 'description': PlutoCell(value: media.fileDescription ?? ''), 'category': PlutoCell(value: _getCategoryName(media.mediaCategoryFk)), 'reproducciones': PlutoCell(value: media.reproducciones), 'duration': PlutoCell(value: media.seconds ?? 0), 'size': PlutoCell(value: _formatFileSize(media.fileSizeBytes)), 'created_at': PlutoCell(value: media.createdAt), 'actions': PlutoCell(value: media.mediaFileId), }, ), ); } } /// Get category name by ID String _getCategoryName(int? categoryId) { if (categoryId == null) return 'Sin categoría'; try { return categories .firstWhere((cat) => cat.mediaCategoriesId == categoryId) .categoryName; } catch (e) { return 'Sin categoría'; } } /// Format file size to human readable String _formatFileSize(int? bytes) { if (bytes == null) return '-'; if (bytes < 1024) return '$bytes B'; if (bytes < 1048576) return '${(bytes / 1024).toStringAsFixed(1)} KB'; if (bytes < 1073741824) return '${(bytes / 1048576).toStringAsFixed(1)} MB'; return '${(bytes / 1073741824).toStringAsFixed(1)} GB'; } // ========== VIDEO UPLOAD ========== /// Select video file from device Future selectVideo() async { try { final ImagePicker picker = ImagePicker(); final XFile? pickedVideo = await picker.pickVideo( source: ImageSource.gallery, ); if (pickedVideo == null) return false; videoName = pickedVideo.name; videoFileExtension = p.extension(pickedVideo.name); webVideoBytes = await pickedVideo.readAsBytes(); // Remove extension from name for title final nameWithoutExt = videoName!.replaceAll(videoFileExtension, ''); tituloController.text = nameWithoutExt; notifyListeners(); return true; } catch (e) { errorMessage = 'Error seleccionando video: $e'; notifyListeners(); return false; } } /// Select poster/thumbnail image Future selectPoster() async { try { final ImagePicker picker = ImagePicker(); final XFile? pickedImage = await picker.pickImage( source: ImageSource.gallery, ); if (pickedImage == null) return false; posterName = pickedImage.name; posterFileExtension = p.extension(pickedImage.name); webPosterBytes = await pickedImage.readAsBytes(); notifyListeners(); return true; } catch (e) { errorMessage = 'Error seleccionando poster: $e'; notifyListeners(); return false; } } /// Upload video to Supabase Storage and create record Future uploadVideo({ required String title, String? description, int? categoryId, int? durationSeconds, }) async { if (webVideoBytes == null || videoName == null) { errorMessage = 'No hay video seleccionado'; notifyListeners(); return false; } try { isLoading = true; notifyListeners(); // 1. Upload video to storage final timestamp = DateTime.now().millisecondsSinceEpoch; final fileName = '${timestamp}_$videoName'; videoStoragePath = 'videos/$fileName'; await supabaseML.storage.from('energymedia').uploadBinary( videoStoragePath!, webVideoBytes!, fileOptions: const FileOptions( cacheControl: '3600', upsert: false, ), ); // 2. Get public URL videoUrl = supabaseML.storage .from('energymedia') .getPublicUrl(videoStoragePath!); // 3. Upload poster if exists int? posterFileId; if (webPosterBytes != null && posterName != null) { posterFileId = await _uploadPoster(); } // 4. Create media_files record final metadataJson = { 'uploaded_at': DateTime.now().toIso8601String(), 'reproducciones': 0, 'original_file_name': videoName, 'duration_seconds': durationSeconds, }; final response = await supabaseML.from('media_files').insert({ 'file_name': fileName, 'title': title, 'file_description': description, 'file_type': 'video', 'mime_type': _getMimeType(videoFileExtension), 'file_extension': videoFileExtension, 'file_size_bytes': webVideoBytes!.length, 'file_url': videoUrl, 'storage_path': videoStoragePath, 'organization_fk': organizationId, 'media_category_fk': categoryId, 'metadata_json': metadataJson, 'seconds': durationSeconds, 'is_public_file': true, 'uploaded_by_user_id': currentUser?.id, }).select(); // 5. Create poster relationship if exists if (posterFileId != null && response.isNotEmpty) { final mediaFileId = response[0]['media_file_id']; await supabaseML.from('media_posters').insert({ 'media_file_id': mediaFileId, 'poster_file_id': posterFileId, }); } // Clean up _clearUploadState(); // Reload data await loadMediaFiles(); isLoading = false; notifyListeners(); return true; } catch (e) { errorMessage = 'Error subiendo video: $e'; isLoading = false; notifyListeners(); print('Error en uploadVideo: $e'); return false; } } /// Upload poster image (internal helper) Future _uploadPoster() async { if (webPosterBytes == null || posterName == null) return null; try { final timestamp = DateTime.now().millisecondsSinceEpoch; final fileName = '${timestamp}_$posterName'; posterStoragePath = 'imagenes/$fileName'; await supabaseML.storage.from('energymedia').uploadBinary( posterStoragePath!, webPosterBytes!, fileOptions: const FileOptions( cacheControl: '3600', upsert: false, ), ); posterUrl = supabaseML.storage .from('energymedia') .getPublicUrl(posterStoragePath!); // Create media_files record for poster final response = await supabaseML.from('media_files').insert({ 'file_name': fileName, 'title': 'Poster', 'file_type': 'image', 'mime_type': _getMimeType(posterFileExtension), 'file_extension': posterFileExtension, 'file_size_bytes': webPosterBytes!.length, 'file_url': posterUrl, 'storage_path': posterStoragePath, 'organization_fk': organizationId, 'is_public_file': true, 'uploaded_by_user_id': currentUser?.id, }).select(); return response[0]['media_file_id'] as int; } catch (e) { print('Error en _uploadPoster: $e'); return null; } } /// Get MIME type from file extension String _getMimeType(String extension) { final ext = extension.toLowerCase().replaceAll('.', ''); switch (ext) { case 'mp4': return 'video/mp4'; case 'webm': return 'video/webm'; case 'mov': return 'video/quicktime'; case 'avi': return 'video/x-msvideo'; case 'jpg': case 'jpeg': return 'image/jpeg'; case 'png': return 'image/png'; case 'gif': return 'image/gif'; default: return 'application/octet-stream'; } } /// Clear upload state void _clearUploadState() { videoName = null; videoUrl = null; videoStoragePath = null; videoFileExtension = ''; webVideoBytes = null; posterName = null; posterUrl = null; posterStoragePath = null; posterFileExtension = ''; webPosterBytes = null; tituloController.clear(); descripcionController.clear(); } // ========== UPDATE METHODS ========== /// Update video title Future updateVideoTitle(int mediaFileId, String title) async { try { await supabaseML .from('media_files') .update({'title': title}) .eq('media_file_id', mediaFileId) .eq('organization_fk', organizationId); await loadMediaFiles(); return true; } catch (e) { errorMessage = 'Error actualizando título: $e'; notifyListeners(); print('Error en updateVideoTitle: $e'); return false; } } /// Update video description Future updateVideoDescription( int mediaFileId, String description) async { try { await supabaseML .from('media_files') .update({'file_description': description}) .eq('media_file_id', mediaFileId) .eq('organization_fk', organizationId); await loadMediaFiles(); return true; } catch (e) { errorMessage = 'Error actualizando descripción: $e'; notifyListeners(); print('Error en updateVideoDescription: $e'); return false; } } /// Update video category Future updateVideoCategory(int mediaFileId, int? categoryId) async { try { await supabaseML .from('media_files') .update({'media_category_fk': categoryId}) .eq('media_file_id', mediaFileId) .eq('organization_fk', organizationId); await loadMediaFiles(); return true; } catch (e) { errorMessage = 'Error actualizando categoría: $e'; notifyListeners(); print('Error en updateVideoCategory: $e'); return false; } } /// Update video metadata Future updateVideoMetadata( int mediaFileId, Map metadata, ) async { try { await supabaseML .from('media_files') .update({'metadata_json': metadata}) .eq('media_file_id', mediaFileId) .eq('organization_fk', organizationId); await loadMediaFiles(); return true; } catch (e) { errorMessage = 'Error actualizando metadata: $e'; notifyListeners(); print('Error en updateVideoMetadata: $e'); return false; } } // ========== DELETE METHODS ========== /// Delete video and its storage files Future deleteVideo(int mediaFileId) async { try { isLoading = true; notifyListeners(); // Get video info final response = await supabaseML .from('media_files') .select() .eq('media_file_id', mediaFileId) .eq('organization_fk', organizationId) .single(); final storagePath = response['storage_path'] as String?; // Delete from storage if path exists if (storagePath != null) { await supabaseML.storage.from('energymedia').remove([storagePath]); } // Delete associated posters final posters = await supabaseML .from('media_posters') .select('poster_file_id') .eq('media_file_id', mediaFileId); for (var poster in posters) { await _deletePosterFile(poster['poster_file_id']); } // Delete database record (cascade will delete posters relationship) await supabaseML .from('media_files') .delete() .eq('media_file_id', mediaFileId) .eq('organization_fk', organizationId); await loadMediaFiles(); isLoading = false; notifyListeners(); return true; } catch (e) { errorMessage = 'Error eliminando video: $e'; isLoading = false; notifyListeners(); print('Error en deleteVideo: $e'); return false; } } /// Delete poster file (internal helper) Future _deletePosterFile(int posterFileId) async { try { final response = await supabaseML .from('media_files') .select('storage_path') .eq('media_file_id', posterFileId) .single(); final storagePath = response['storage_path'] as String?; if (storagePath != null) { await supabaseML.storage.from('energymedia').remove([storagePath]); } await supabaseML .from('media_files') .delete() .eq('media_file_id', posterFileId); } catch (e) { print('Error en _deletePosterFile: $e'); } } // ========== ANALYTICS METHODS ========== /// Increment view count Future incrementReproduccion(int mediaFileId) async { try { // Get current metadata final response = await supabaseML .from('media_files') .select('metadata_json') .eq('media_file_id', mediaFileId) .eq('organization_fk', organizationId) .single(); final metadata = response['metadata_json'] as Map? ?? {}; final currentCount = metadata['reproducciones'] ?? 0; metadata['reproducciones'] = currentCount + 1; metadata['last_viewed_at'] = DateTime.now().toIso8601String(); await updateVideoMetadata(mediaFileId, metadata); return true; } catch (e) { print('Error en incrementReproduccion: $e'); return false; } } /// Get dashboard statistics Future> getDashboardStats() async { try { // Total videos final totalVideos = mediaFiles.length; // Total reproducciones int totalReproducciones = 0; for (var media in mediaFiles) { totalReproducciones += media.reproducciones; } // Most viewed video MediaFileModel? mostViewed; if (mediaFiles.isNotEmpty) { mostViewed = mediaFiles.reduce((curr, next) => curr.reproducciones > next.reproducciones ? curr : next); } // Videos by category Map videosByCategory = {}; for (var media in mediaFiles) { final categoryName = _getCategoryName(media.mediaCategoryFk); videosByCategory[categoryName] = (videosByCategory[categoryName] ?? 0) + 1; } // Most viewed category String? mostViewedCategory; if (videosByCategory.isNotEmpty) { mostViewedCategory = videosByCategory.entries .reduce((a, b) => a.value > b.value ? a : b) .key; } return { 'total_videos': totalVideos, 'total_reproducciones': totalReproducciones, 'most_viewed_video': mostViewed?.toMap(), 'videos_by_category': videosByCategory, 'most_viewed_category': mostViewedCategory, 'total_categories': categories.length, }; } catch (e) { print('Error en getDashboardStats: $e'); return {}; } } // ========== SEARCH & FILTER ========== /// Search videos by title or description void searchVideos(String query) { if (query.isEmpty) { _buildPlutoRows(); notifyListeners(); return; } videosRows.clear(); final filteredMedia = mediaFiles.where((media) { final title = (media.title ?? media.fileName).toLowerCase(); final description = (media.fileDescription ?? '').toLowerCase(); final searchQuery = query.toLowerCase(); return title.contains(searchQuery) || description.contains(searchQuery); }).toList(); for (var media in filteredMedia) { videosRows.add( PlutoRow( cells: { 'id': PlutoCell(value: media.mediaFileId), 'thumbnail': PlutoCell(value: media.fileUrl), 'title': PlutoCell(value: media.title ?? media.fileName), 'description': PlutoCell(value: media.fileDescription ?? ''), 'category': PlutoCell(value: _getCategoryName(media.mediaCategoryFk)), 'reproducciones': PlutoCell(value: media.reproducciones), 'duration': PlutoCell(value: media.seconds ?? 0), 'size': PlutoCell(value: _formatFileSize(media.fileSizeBytes)), 'created_at': PlutoCell(value: media.createdAt), 'actions': PlutoCell(value: media.mediaFileId), }, ), ); } notifyListeners(); } // ========== CLEANUP ========== @override void dispose() { busquedaVideoController.dispose(); tituloController.dispose(); descripcionController.dispose(); super.dispose(); } }