import 'dart:async';
import 'dart:convert';
import 'package:crypto/crypto.dart';
import '../models/post_models.dart';
import '../models/invitation_models.dart';

class CachedData<T> {
  final T data;
  final DateTime timestamp;
  final String jsonHash;

  CachedData({
    required this.data,
    required this.timestamp,
    required this.jsonHash,
  });

  bool isExpired(Duration maxAge) {
    return DateTime.now().difference(timestamp) > maxAge;
  }
}

class CacheService {
  static final Map<String, CachedData> _cache = {};
  static final Map<String, Timer> _pollingTimers = {};
  static final Map<String, StreamController> _streamControllers = {};

  // Cache durations
  static const Duration _postsCacheDuration = Duration(seconds: 30);
  static const Duration _invitationsCacheDuration = Duration(seconds: 30);
  static const Duration _userCacheDuration = Duration(hours: 6);
  static const Duration _userPostsCacheDuration = Duration(seconds: 30);

  // Polling intervals
  static const Duration _postsPollingInterval = Duration(seconds: 5);
  static const Duration _invitationsPollingInterval = Duration(seconds: 5);
  static const Duration _userPollingInterval = Duration(hours: 3);
  static const Duration _userPostsPollingInterval = Duration(seconds: 5);

  /// Get cached data if available and not expired, otherwise return null
  static T? getCached<T>(String key) {
    final cached = _cache[key];
    if (cached != null) {
      return cached.data as T?;
    }
    return null;
  }

  /// Get cached data regardless of expiration (for showing stale data while loading)
  static T? getCachedStale<T>(String key) {
    final cached = _cache[key];
    if (cached != null) {
      return cached.data as T?;
    }
    return null;
  }

  /// Check if cache is expired
  static bool isCacheExpired(String key, Duration maxAge) {
    final cached = _cache[key];
    if (cached == null) return true;
    return cached.isExpired(maxAge);
  }

  /// Set cached data with JSON hash for change detection
  static void setCached<T>(String key, T data) {
    final jsonHash = _generateDataHash(data);

    _cache[key] = CachedData(
      data: data,
      timestamp: DateTime.now(),
      jsonHash: jsonHash,
    );
  }

  /// Check if data has changed by comparing hashes
  static bool hasDataChanged(String key, dynamic newData) {
    final cached = _cache[key];
    if (cached == null) return true;

    final newJsonHash = _generateDataHash(newData);
    final hasChanged = cached.jsonHash != newJsonHash;

    if (hasChanged) {
      print(
        'Data changed for $key: old hash ${cached.jsonHash}, new hash $newJsonHash',
      );
    } else {
      print('No data changes for $key: hash remains ${cached.jsonHash}');
    }

    return hasChanged;
  }

  /// Generate a reliable hash for data comparison
  static String _generateDataHash(dynamic data) {
    try {
      String jsonString;

      if (data is List) {
        // Handle lists of objects (like List<Post>)
        final jsonList = data.map((item) {
          if (item is Map) {
            return item;
          } else if (item.runtimeType.toString().contains('Post')) {
            // Call toJson() on Post objects
            return (item as dynamic).toJson();
          } else if (item.runtimeType.toString().contains('Invitation')) {
            // Call toJson() on Invitation objects
            return (item as dynamic).toJson();
          } else {
            return item.toString();
          }
        }).toList();
        jsonString = jsonEncode(jsonList);
      } else if (data is Map) {
        jsonString = jsonEncode(data);
      } else if (data.runtimeType.toString().contains('Post') ||
          data.runtimeType.toString().contains('Invitation')) {
        // Call toJson() on model objects
        jsonString = jsonEncode((data as dynamic).toJson());
      } else {
        jsonString = data.toString();
      }

      // Generate MD5 hash for comparison
      final bytes = utf8.encode(jsonString);
      final digest = md5.convert(bytes);
      final hashResult = digest.toString();

      // Debug: Show first 100 chars of JSON for debugging
      final debugJson = jsonString.length > 100
          ? jsonString.substring(0, 100) + '...'
          : jsonString;
      print('Generated hash $hashResult for JSON: $debugJson');

      return hashResult;
    } catch (e) {
      print('Error generating data hash: $e');
      // Fallback to simple string hash
      return data.toString().hashCode.toString();
    }
  }

  /// Get stream for cache updates
  static Stream<T> getStream<T>(String key) {
    if (!_streamControllers.containsKey(key)) {
      _streamControllers[key] = StreamController<T>.broadcast();
    }
    return (_streamControllers[key] as StreamController<T>).stream;
  }

  /// Notify stream listeners of cache updates (safely) - Force UI refresh
  static void notifyStreamListeners<T>(String key, T data) {
    try {
      // Ensure we have a stream controller
      if (!_streamControllers.containsKey(key)) {
        print('Creating new stream controller for $key');
        _streamControllers[key] = StreamController<T>.broadcast();
      }
      
      final controller = _streamControllers[key] as StreamController<T>;
      
      if (!controller.isClosed) {
        print('Notifying stream listeners for $key with ${data.runtimeType}');
        controller.add(data);
      } else {
        print('Stream controller for $key is closed, creating new one');
        _streamControllers[key] = StreamController<T>.broadcast();
        (_streamControllers[key] as StreamController<T>).add(data);
      }
    } catch (e) {
      print('Error notifying stream listeners for $key: $e');
      // Recreate controller on error
      try {
        _streamControllers[key]?.close();
        _streamControllers[key] = StreamController<T>.broadcast();
        (_streamControllers[key] as StreamController<T>).add(data);
      } catch (recreateError) {
        print('Failed to recreate stream controller for $key: $recreateError');
      }
    }
  }

  /// Start polling for a specific endpoint (silent - no UI notifications)
  static void startPolling(
    String key,
    Future<Map<String, dynamic>> Function() fetchFunction,
    Duration interval,
    Duration cacheMaxAge,
  ) {
    // Cancel existing timer if any
    stopPolling(key);

    _pollingTimers[key] = Timer.periodic(interval, (timer) async {
      try {
        print('Polling $key...');
        final result = await fetchFunction();

        if (result['success'] == true) {
          // Use the parsed data from the result (e.g., result['posts'] for posts)
          final data = result['posts'] ?? result['invitations'] ?? result['data'];
          print('📊 Polling $key got data type: ${data.runtimeType}, length: ${data is List ? data.length : 'N/A'}');

          // Check if data has changed BEFORE updating cache
          final dataChanged = hasDataChanged(key, data);
          
          if (dataChanged) {
            print(
              '🔄 Data changed for $key, updating cache and forcing UI refresh',
            );
            setCached(key, data);
            // Force refresh by notifying stream listeners with appropriate types
            if (key == postsAllKey && data is List<Post>) {
              print('📱 Notifying posts stream with ${data.length} posts');
              notifyStreamListeners<List<Post>>(key, data);
            } else if (key == userPostsKey && data is List<Post>) {
              print('📱 Notifying user posts stream with ${data.length} posts');
              notifyStreamListeners<List<Post>>(key, data);
            } else if (key == invitationsAllKey && data is InvitationsData) {
              print('📱 Notifying invitations stream');
              notifyStreamListeners<InvitationsData>(key, data);
            } else {
              print('📱 Notifying generic stream for $key with ${data.runtimeType}');
              notifyStreamListeners<dynamic>(key, data);
            }
          } else {
            print('No data changes for $key - not notifying streams');
          }
        } else {
          print('Polling failed for $key: ${result['message']}');
        }
      } catch (e) {
        print('Polling error for $key: $e');
      }
    });
  }

  /// Stop polling for a specific endpoint
  static void stopPolling(String key) {
    _pollingTimers[key]?.cancel();
    _pollingTimers.remove(key);
  }

  /// Stop all polling
  static void stopAllPolling() {
    for (final timer in _pollingTimers.values) {
      timer.cancel();
    }
    _pollingTimers.clear();
  }

  /// Clear all caches
  static void clearAll() {
    _cache.clear();
    stopAllPolling();
    _cleanupStreamControllers();
  }

  /// Safely cleanup stream controllers
  static void _cleanupStreamControllers() {
    for (final controller in _streamControllers.values) {
      if (!controller.isClosed) {
        controller.close();
      }
    }
    _streamControllers.clear();
  }

  /// Clean up specific stream controller
  static void cleanupStream(String key) {
    if (_streamControllers.containsKey(key)) {
      if (!_streamControllers[key]!.isClosed) {
        _streamControllers[key]!.close();
      }
      _streamControllers.remove(key);
    }
  }

  /// Clear specific cache
  static void clearCache(String key) {
    _cache.remove(key);
  }

  /// Get cache info for debugging
  static Map<String, dynamic> getCacheInfo() {
    return {
      'cacheSize': _cache.length,
      'activePollers': _pollingTimers.length,
      'activeStreams': _streamControllers.length,
      'cacheKeys': _cache.keys.toList(),
    };
  }

  // Predefined cache keys
  static const String postsAllKey = 'posts_all';
  static const String invitationsAllKey = 'invitations_all';
  static const String userProfileKey = 'user_profile';
  static const String userPostsKey = 'user_posts';

  // Helper methods for specific cache durations
  static Duration get postsCacheDuration => _postsCacheDuration;
  static Duration get invitationsCacheDuration => _invitationsCacheDuration;
  static Duration get userCacheDuration => _userCacheDuration;
  static Duration get userPostsCacheDuration => _userPostsCacheDuration;

  static Duration get postsPollingInterval => _postsPollingInterval;
  static Duration get invitationsPollingInterval => _invitationsPollingInterval;
  static Duration get userPollingInterval => _userPollingInterval;
  static Duration get userPostsPollingInterval => _userPostsPollingInterval;
}