/**
 * DailyNotificationStorage.java
 * 
 * Storage management for notification content and settings
 * Implements tiered storage: Key-Value (quick) + DB (structured) + Files (large assets)
 * 
 * @author Matthew Raymer
 * @version 1.0.0
 */

package org.timesafari.dailynotification;

import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import java.io.File;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Manages storage for notification content and settings
 * 
 * This class implements the tiered storage approach:
 * - Tier 1: SharedPreferences for quick access to settings and recent data
 * - Tier 2: In-memory cache for structured notification content
 * - Tier 3: File system for large assets (future use)
 */
public class DailyNotificationStorage {
    
    private static final String TAG = "DailyNotificationStorage";
    private static final String PREFS_NAME = "DailyNotificationPrefs";
    private static final String KEY_NOTIFICATIONS = "notifications";
    private static final String KEY_SETTINGS = "settings";
    private static final String KEY_LAST_FETCH = "last_fetch";
    private static final String KEY_ADAPTIVE_SCHEDULING = "adaptive_scheduling";
    
    private static final int MAX_CACHE_SIZE = 100; // Maximum notifications to keep in memory
    private static final long CACHE_CLEANUP_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
    private static final int MAX_STORAGE_ENTRIES = 100; // Maximum total storage entries
    private static final long RETENTION_PERIOD_MS = 14 * 24 * 60 * 60 * 1000; // 14 days
    private static final int BATCH_CLEANUP_SIZE = 50; // Clean up in batches
    
    private final Context context;
    private final SharedPreferences prefs;
    private final Gson gson;
    private final ConcurrentHashMap<String, NotificationContent> notificationCache;
    private final List<NotificationContent> notificationList;
    
    /**
     * Constructor
     * 
     * @param context Application context
     */
    public DailyNotificationStorage(Context context) {
        this.context = context;
        this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
        // Create Gson with custom deserializer for NotificationContent
        com.google.gson.GsonBuilder gsonBuilder = new com.google.gson.GsonBuilder();
        gsonBuilder.registerTypeAdapter(NotificationContent.class, new NotificationContent.NotificationContentDeserializer());
        this.gson = gsonBuilder.create();
        this.notificationCache = new ConcurrentHashMap<>();
        this.notificationList = Collections.synchronizedList(new ArrayList<>());
        
        loadNotificationsFromStorage();
        cleanupOldNotifications();
        // Remove duplicates on startup and cancel their alarms/workers
        java.util.List<String> removedIds = deduplicateNotifications();
        cancelRemovedNotifications(removedIds);
    }
    
    /**
     * Save notification content to storage
     * 
     * @param content Notification content to save
     */
    public void saveNotificationContent(NotificationContent content) {
        try {
            Log.d(TAG, "DN|STORAGE_SAVE_START id=" + content.getId());
            
            // Add to cache
            notificationCache.put(content.getId(), content);
            
            // Add to list and sort by scheduled time
            synchronized (notificationList) {
                notificationList.removeIf(n -> n.getId().equals(content.getId()));
                notificationList.add(content);
                Collections.sort(notificationList, 
                    Comparator.comparingLong(NotificationContent::getScheduledTime));
                
                // Apply storage cap and retention policy
                enforceStorageLimits();
            }
            
            // Persist to SharedPreferences
            saveNotificationsToStorage();
            
            Log.d(TAG, "DN|STORAGE_SAVE_OK id=" + content.getId() + " total=" + notificationList.size());
            
        } catch (Exception e) {
            Log.e(TAG, "Error saving notification content", e);
        }
    }
    
    /**
     * Get notification content by ID
     * 
     * @param id Notification ID
     * @return Notification content or null if not found
     */
    public NotificationContent getNotificationContent(String id) {
        return notificationCache.get(id);
    }
    
    /**
     * Get the last notification that was delivered
     * 
     * @return Last notification or null if none exists
     */
    public NotificationContent getLastNotification() {
        synchronized (notificationList) {
            if (notificationList.isEmpty()) {
                return null;
            }
            
            // Find the most recent delivered notification
            long currentTime = System.currentTimeMillis();
            for (int i = notificationList.size() - 1; i >= 0; i--) {
                NotificationContent notification = notificationList.get(i);
                if (notification.getScheduledTime() <= currentTime) {
                    return notification;
                }
            }
            
            return null;
        }
    }
    
    /**
     * Get all notifications
     * 
     * @return List of all notifications
     */
    public List<NotificationContent> getAllNotifications() {
        synchronized (notificationList) {
            return new ArrayList<>(notificationList);
        }
    }
    
    /**
     * Get notifications that are ready to be displayed
     * 
     * @return List of ready notifications
     */
    public List<NotificationContent> getReadyNotifications() {
        List<NotificationContent> readyNotifications = new ArrayList<>();
        long currentTime = System.currentTimeMillis();
        
        synchronized (notificationList) {
            for (NotificationContent notification : notificationList) {
                if (notification.isReadyToDisplay()) {
                    readyNotifications.add(notification);
                }
            }
        }
        
        return readyNotifications;
    }
    
    /**
     * Get the next scheduled notification
     * 
     * @return Next notification or null if none scheduled
     */
    public NotificationContent getNextNotification() {
        synchronized (notificationList) {
            long currentTime = System.currentTimeMillis();
            
            for (NotificationContent notification : notificationList) {
                if (notification.getScheduledTime() > currentTime) {
                    return notification;
                }
            }
            
            return null;
        }
    }
    
    /**
     * Remove notification by ID
     * 
     * @param id Notification ID to remove
     */
    public void removeNotification(String id) {
        try {
            Log.d(TAG, "Removing notification: " + id);
            
            notificationCache.remove(id);
            
            synchronized (notificationList) {
                notificationList.removeIf(n -> n.getId().equals(id));
            }
            
            saveNotificationsToStorage();
            
            Log.d(TAG, "Notification removed successfully");
            
        } catch (Exception e) {
            Log.e(TAG, "Error removing notification", e);
        }
    }
    
    /**
     * Clear all notifications
     */
    public void clearAllNotifications() {
        try {
            Log.d(TAG, "Clearing all notifications");
            
            notificationCache.clear();
            
            synchronized (notificationList) {
                notificationList.clear();
            }
            
            saveNotificationsToStorage();
            
            Log.d(TAG, "All notifications cleared successfully");
            
        } catch (Exception e) {
            Log.e(TAG, "Error clearing notifications", e);
        }
    }
    
    /**
     * Get notification count
     * 
     * @return Number of notifications
     */
    public int getNotificationCount() {
        return notificationCache.size();
    }
    
    /**
     * Check if storage is empty
     * 
     * @return true if no notifications exist
     */
    public boolean isEmpty() {
        return notificationCache.isEmpty();
    }
    
    /**
     * Set sound enabled setting
     * 
     * @param enabled true to enable sound
     */
    public void setSoundEnabled(boolean enabled) {
        SharedPreferences.Editor editor = prefs.edit();
        editor.putBoolean("sound_enabled", enabled);
        editor.apply();
        
        Log.d(TAG, "Sound setting updated: " + enabled);
    }
    
    /**
     * Get sound enabled setting
     * 
     * @return true if sound is enabled
     */
    public boolean isSoundEnabled() {
        return prefs.getBoolean("sound_enabled", true);
    }
    
    /**
     * Set notification priority
     * 
     * @param priority Priority string (high, default, low)
     */
    public void setPriority(String priority) {
        SharedPreferences.Editor editor = prefs.edit();
        editor.putString("priority", priority);
        editor.apply();
        
        Log.d(TAG, "Priority setting updated: " + priority);
    }
    
    /**
     * Get notification priority
     * 
     * @return Priority string
     */
    public String getPriority() {
        return prefs.getString("priority", "default");
    }
    
    /**
     * Set timezone setting
     * 
     * @param timezone Timezone identifier
     */
    public void setTimezone(String timezone) {
        SharedPreferences.Editor editor = prefs.edit();
        editor.putString("timezone", timezone);
        editor.apply();
        
        Log.d(TAG, "Timezone setting updated: " + timezone);
    }
    
    /**
     * Get timezone setting
     * 
     * @return Timezone identifier
     */
    public String getTimezone() {
        return prefs.getString("timezone", "UTC");
    }
    
    /**
     * Set adaptive scheduling enabled
     * 
     * @param enabled true to enable adaptive scheduling
     */
    public void setAdaptiveSchedulingEnabled(boolean enabled) {
        SharedPreferences.Editor editor = prefs.edit();
        editor.putBoolean(KEY_ADAPTIVE_SCHEDULING, enabled);
        editor.apply();
        
        Log.d(TAG, "Adaptive scheduling setting updated: " + enabled);
    }
    
    /**
     * Check if adaptive scheduling is enabled
     * 
     * @return true if adaptive scheduling is enabled
     */
    public boolean isAdaptiveSchedulingEnabled() {
        return prefs.getBoolean(KEY_ADAPTIVE_SCHEDULING, true);
    }
    
    /**
     * Set last fetch timestamp
     * 
     * @param timestamp Last fetch time in milliseconds
     */
    public void setLastFetchTime(long timestamp) {
        SharedPreferences.Editor editor = prefs.edit();
        editor.putLong(KEY_LAST_FETCH, timestamp);
        editor.apply();
        
        Log.d(TAG, "Last fetch time updated: " + timestamp);
    }
    
    /**
     * Get last fetch timestamp
     * 
     * @return Last fetch time in milliseconds
     */
    public long getLastFetchTime() {
        return prefs.getLong(KEY_LAST_FETCH, 0);
    }
    
    /**
     * Check if it's time to fetch new content
     * 
     * @return true if fetch is needed
     */
    public boolean shouldFetchNewContent() {
        long lastFetch = getLastFetchTime();
        long currentTime = System.currentTimeMillis();
        long timeSinceLastFetch = currentTime - lastFetch;
        
        // Fetch if more than 12 hours have passed
        return timeSinceLastFetch > 12 * 60 * 60 * 1000;
    }
    
    /**
     * Load notifications from persistent storage
     */
    private void loadNotificationsFromStorage() {
        try {
            String notificationsJson = prefs.getString(KEY_NOTIFICATIONS, "[]");
            Log.d(TAG, "Loading notifications from storage: " + notificationsJson);
            Type type = new TypeToken<ArrayList<NotificationContent>>(){}.getType();
            List<NotificationContent> notifications = gson.fromJson(notificationsJson, type);
            
            if (notifications != null) {
                for (NotificationContent notification : notifications) {
                    notificationCache.put(notification.getId(), notification);
                    notificationList.add(notification);
                }
                
                // Sort by scheduled time
                Collections.sort(notificationList, 
                    Comparator.comparingLong(NotificationContent::getScheduledTime));
                
                Log.d(TAG, "Loaded " + notifications.size() + " notifications from storage");
            }
            
        } catch (Exception e) {
            Log.e(TAG, "Error loading notifications from storage", e);
        }
    }
    
    /**
     * Save notifications to persistent storage
     */
    private void saveNotificationsToStorage() {
        try {
            List<NotificationContent> notifications;
            synchronized (notificationList) {
                notifications = new ArrayList<>(notificationList);
            }
            
            String notificationsJson = gson.toJson(notifications);
            SharedPreferences.Editor editor = prefs.edit();
            editor.putString(KEY_NOTIFICATIONS, notificationsJson);
            editor.apply();
            
            Log.d(TAG, "Saved " + notifications.size() + " notifications to storage");
            
        } catch (Exception e) {
            Log.e(TAG, "Error saving notifications to storage", e);
        }
    }
    
    /**
     * Clean up old notifications to prevent memory bloat
     */
    private void cleanupOldNotifications() {
        try {
            long currentTime = System.currentTimeMillis();
            long cutoffTime = currentTime - (7 * 24 * 60 * 60 * 1000); // 7 days ago
            
            synchronized (notificationList) {
                notificationList.removeIf(notification -> 
                    notification.getScheduledTime() < cutoffTime);
            }
            
            // Update cache to match
            notificationCache.clear();
            for (NotificationContent notification : notificationList) {
                notificationCache.put(notification.getId(), notification);
            }
            
            // Limit cache size
            if (notificationCache.size() > MAX_CACHE_SIZE) {
                List<NotificationContent> sortedNotifications = new ArrayList<>(notificationList);
                Collections.sort(sortedNotifications, 
                    Comparator.comparingLong(NotificationContent::getScheduledTime));
                
                int toRemove = sortedNotifications.size() - MAX_CACHE_SIZE;
                for (int i = 0; i < toRemove; i++) {
                    NotificationContent notification = sortedNotifications.get(i);
                    notificationCache.remove(notification.getId());
                }
                
                notificationList.clear();
                notificationList.addAll(sortedNotifications.subList(toRemove, sortedNotifications.size()));
            }
            
            saveNotificationsToStorage();
            
            Log.d(TAG, "Cleanup completed. Cache size: " + notificationCache.size());
            
        } catch (Exception e) {
            Log.e(TAG, "Error during cleanup", e);
        }
    }
    
    /**
     * Get storage statistics
     * 
     * @return Storage statistics as a string
     */
    public String getStorageStats() {
        return String.format("Notifications: %d, Cache size: %d, Last fetch: %d",
            notificationList.size(),
            notificationCache.size(),
            getLastFetchTime());
    }
    
    /**
     * Remove duplicate notifications (same scheduledTime within tolerance)
     * 
     * Keeps the most recently created notification for each scheduledTime,
     * removes older duplicates to prevent accumulation.
     * 
     * @return List of notification IDs that were removed (for cancellation of alarms/workers)
     */
    public java.util.List<String> deduplicateNotifications() {
        try {
            long toleranceMs = 60 * 1000; // 1 minute tolerance
            java.util.Map<Long, NotificationContent> scheduledTimeMap = new java.util.HashMap<>();
            java.util.List<String> idsToRemove = new java.util.ArrayList<>();
            
            synchronized (notificationList) {
                // First pass: find all duplicates, keep the one with latest fetchedAt
                for (NotificationContent notification : notificationList) {
                    long scheduledTime = notification.getScheduledTime();
                    boolean foundMatch = false;
                    
                    for (java.util.Map.Entry<Long, NotificationContent> entry : scheduledTimeMap.entrySet()) {
                        if (Math.abs(entry.getKey() - scheduledTime) <= toleranceMs) {
                            // Found a duplicate - keep the one with latest fetchedAt
                            if (notification.getFetchedAt() > entry.getValue().getFetchedAt()) {
                                idsToRemove.add(entry.getValue().getId());
                                entry.setValue(notification);
                            } else {
                                idsToRemove.add(notification.getId());
                            }
                            foundMatch = true;
                            break;
                        }
                    }
                    
                    if (!foundMatch) {
                        scheduledTimeMap.put(scheduledTime, notification);
                    }
                }
                
                // Remove duplicates
                if (!idsToRemove.isEmpty()) {
                    notificationList.removeIf(n -> idsToRemove.contains(n.getId()));
                    for (String id : idsToRemove) {
                        notificationCache.remove(id);
                    }
                    saveNotificationsToStorage();
                    Log.i(TAG, "DN|DEDUPE_CLEANUP removed=" + idsToRemove.size() + " duplicates");
                }
            }
            
            return idsToRemove;
        } catch (Exception e) {
            Log.e(TAG, "Error during deduplication", e);
            return new java.util.ArrayList<>();
        }
    }
    
    /**
     * Cancel alarms and workers for removed notification IDs
     * 
     * This ensures that when notifications are removed (e.g., during deduplication),
     * their associated alarms and WorkManager workers are also cancelled to prevent
     * zombie workers trying to display non-existent notifications.
     * 
     * @param removedIds List of notification IDs that were removed
     */
    private void cancelRemovedNotifications(java.util.List<String> removedIds) {
        if (removedIds == null || removedIds.isEmpty()) {
            return;
        }
        
        try {
            // Cancel alarms for removed notifications
            DailyNotificationScheduler scheduler = new DailyNotificationScheduler(
                context,
                (android.app.AlarmManager) context.getSystemService(Context.ALARM_SERVICE)
            );
            
            for (String id : removedIds) {
                scheduler.cancelNotification(id);
            }
            
            // Note: WorkManager workers can't be cancelled by notification ID directly
            // Workers will handle missing content gracefully by returning Result.success()
            // (see DailyNotificationWorker.handleDisplayNotification - it returns success for missing content)
            // This prevents retry loops for notifications removed during deduplication
            
            Log.i(TAG, "DN|DEDUPE_CLEANUP cancelled alarms for " + removedIds.size() + " removed notifications");
        } catch (Exception e) {
            Log.e(TAG, "DN|DEDUPE_CLEANUP_ERR failed to cancel alarms/workers", e);
        }
    }
    
    /**
     * Enforce storage limits and retention policy
     * 
     * This method implements both storage capping (max entries) and retention policy
     * (remove old entries) to prevent unbounded growth.
     */
    private void enforceStorageLimits() {
        try {
            long currentTime = System.currentTimeMillis();
            int initialSize = notificationList.size();
            int removedCount = 0;
            
            // First, remove expired entries (older than retention period)
            notificationList.removeIf(notification -> {
                long age = currentTime - notification.getScheduledTime();
                return age > RETENTION_PERIOD_MS;
            });
            
            removedCount = initialSize - notificationList.size();
            if (removedCount > 0) {
                Log.d(TAG, "DN|RETENTION_CLEANUP removed=" + removedCount + " expired_entries");
            }
            
            // Then, enforce storage cap by removing oldest entries if over limit
            while (notificationList.size() > MAX_STORAGE_ENTRIES) {
                NotificationContent oldest = notificationList.remove(0);
                notificationCache.remove(oldest.getId());
                removedCount++;
            }
            
            if (removedCount > 0) {
                Log.i(TAG, "DN|STORAGE_LIMITS_ENFORCED removed=" + removedCount + 
                          " total=" + notificationList.size() + 
                          " max=" + MAX_STORAGE_ENTRIES);
            }
            
        } catch (Exception e) {
            Log.e(TAG, "DN|STORAGE_LIMITS_ERR err=" + e.getMessage(), e);
        }
    }
    
    /**
     * Perform batch cleanup of old notifications
     * 
     * This method can be called periodically to clean up old notifications
     * in batches to avoid blocking the main thread.
     * 
     * @return Number of notifications removed
     */
    public int performBatchCleanup() {
        try {
            long currentTime = System.currentTimeMillis();
            int removedCount = 0;
            int batchSize = 0;
            
            synchronized (notificationList) {
                java.util.Iterator<NotificationContent> iterator = notificationList.iterator();
                
                while (iterator.hasNext() && batchSize < BATCH_CLEANUP_SIZE) {
                    NotificationContent notification = iterator.next();
                    long age = currentTime - notification.getScheduledTime();
                    
                    if (age > RETENTION_PERIOD_MS) {
                        iterator.remove();
                        notificationCache.remove(notification.getId());
                        removedCount++;
                        batchSize++;
                    }
                }
            }
            
            if (removedCount > 0) {
                saveNotificationsToStorage();
                Log.i(TAG, "DN|BATCH_CLEANUP_OK removed=" + removedCount + 
                          " batch_size=" + batchSize + 
                          " remaining=" + notificationList.size());
            }
            
            return removedCount;
            
        } catch (Exception e) {
            Log.e(TAG, "DN|BATCH_CLEANUP_ERR err=" + e.getMessage(), e);
            return 0;
        }
    }
}
