/**
 * TimeSafariIntegrationManager.java
 *
 * Purpose: Extract all TimeSafari-specific orchestration from DailyNotificationPlugin
 * into a single cohesive service. The plugin becomes a thin facade that delegates here.
 *
 * Responsibilities (high-level):
 *  - Maintain API server URL & identity (DID/JWT) lifecycle
 *  - Coordinate ETag/JWT/fetcher and (re)fetch schedules
 *  - Bridge Storage <-> Scheduler (save content, arm alarms)
 *  - Offer "status" snapshot for the plugin's public API
 *
 * Non-responsibilities:
 *  - AlarmManager details (kept in DailyNotificationScheduler)
 *  - Notification display (Receiver/Worker)
 *  - Permission prompts (PermissionManager)
 *
 * Notes:
 *  - This file intentionally contains scaffolding methods and TODO tags showing
 *    where the extracted logic from DailyNotificationPlugin should land.
 *  - Keep all Android-side I/O off the main thread unless annotated @MainThread.
 *
 * @author Matthew Raymer
 * @version 1.0.0
 */

package org.timesafari.dailynotification;

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

import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import org.json.JSONArray;
import org.json.JSONException;

/**
 * TimeSafari Integration Manager
 * 
 * Centralizes TimeSafari-specific integration logic extracted from DailyNotificationPlugin
 */
public final class TimeSafariIntegrationManager {

    /**
     * Logger interface for dependency injection
     */
    public interface Logger {
        void d(String msg);
        void w(String msg);
        void e(String msg, @Nullable Throwable t);
        void i(String msg);
    }

    /**
     * Status snapshot for plugin status() method
     */
    public static final class StatusSnapshot {
        public final boolean notificationsGranted;
        public final boolean exactAlarmCapable;
        public final String channelId;
        public final int channelImportance; // NotificationManager.IMPORTANCE_* constant
        public final @Nullable String activeDid;
        public final @Nullable String apiServerUrl;

        public StatusSnapshot(
                boolean notificationsGranted,
                boolean exactAlarmCapable,
                String channelId,
                int channelImportance,
                @Nullable String activeDid,
                @Nullable String apiServerUrl
        ) {
            this.notificationsGranted = notificationsGranted;
            this.exactAlarmCapable = exactAlarmCapable;
            this.channelId = channelId;
            this.channelImportance = channelImportance;
            this.activeDid = activeDid;
            this.apiServerUrl = apiServerUrl;
        }
    }

    private static final String TAG = "TimeSafariIntegrationManager";

    private final Context appContext;
    private final DailyNotificationStorage storage;
    private final DailyNotificationScheduler scheduler;
    private final DailyNotificationETagManager eTagManager;
    private final DailyNotificationJWTManager jwtManager;
    private final EnhancedDailyNotificationFetcher fetcher;
    private final PermissionManager permissionManager;
    private final ChannelManager channelManager;
    private final DailyNotificationTTLEnforcer ttlEnforcer;

    private final Executor io; // single-threaded coordination to preserve ordering
    private final Logger logger;

    // Mutable runtime settings
    private volatile @Nullable String apiServerUrl;
    private volatile @Nullable String activeDid;

    /**
     * Constructor
     */
    public TimeSafariIntegrationManager(
            @NonNull Context context,
            @NonNull DailyNotificationStorage storage,
            @NonNull DailyNotificationScheduler scheduler,
            @NonNull DailyNotificationETagManager eTagManager,
            @NonNull DailyNotificationJWTManager jwtManager,
            @NonNull EnhancedDailyNotificationFetcher fetcher,
            @NonNull PermissionManager permissionManager,
            @NonNull ChannelManager channelManager,
            @NonNull DailyNotificationTTLEnforcer ttlEnforcer,
            @NonNull Logger logger
    ) {
        this.appContext = context.getApplicationContext();
        this.storage = storage;
        this.scheduler = scheduler;
        this.eTagManager = eTagManager;
        this.jwtManager = jwtManager;
        this.fetcher = fetcher;
        this.permissionManager = permissionManager;
        this.channelManager = channelManager;
        this.ttlEnforcer = ttlEnforcer;
        this.logger = logger;
        this.io = Executors.newSingleThreadExecutor();
        
        logger.d("TimeSafariIntegrationManager initialized");
    }

    /* ============================================================
     * Lifecycle / one-time initialization
     * ============================================================ */

    /** Call from Plugin.load() after constructing all managers. */
    @MainThread
    public void onLoad() {
        logger.d("TS: onLoad()");
        // Ensure channel exists once at startup (keep ChannelManager as the single source of truth)
        try {
            channelManager.ensureChannelExists(); // No Context param needed
        } catch (Exception ex) {
            logger.w("TS: ensureChannelExists failed; will rely on lazy creation");
        }
        // Wire TTL enforcer into scheduler (hard-fail at arm time)
        scheduler.setTTLEnforcer(ttlEnforcer);
        logger.i("TS: onLoad() completed - channel ensured, TTL enforcer wired");
    }

    /* ============================================================
     * Identity & server configuration
     * ============================================================ */

    /**
     * Set API server URL for TimeSafari endpoints
     */
    public void setApiServerUrl(@Nullable String url) {
        this.apiServerUrl = url;
        if (url != null) {
            fetcher.setApiServerUrl(url);
            logger.d("TS: API server set → " + url);
        } else {
            logger.w("TS: API server URL cleared");
        }
    }

    /**
     * Get current API server URL
     */
    @Nullable
    public String getApiServerUrl() {
        return apiServerUrl;
    }

    /**
     * Sets the active DID (identity). If DID changes, clears caches/ETags and re-syncs.
     */
    public void setActiveDid(@Nullable String did) {
        final String old = this.activeDid;
        this.activeDid = did;

        if (!Objects.equals(old, did)) {
            logger.d("TS: DID changed: " + (old != null ? old.substring(0, Math.min(20, old.length())) + "..." : "null") + 
                     " → " + (did != null ? did.substring(0, Math.min(20, did.length())) + "..." : "null"));
            onActiveDidChanged(old, did);
        } else {
            logger.d("TS: DID unchanged: " + (did != null ? did.substring(0, Math.min(20, did.length())) + "..." : "null"));
        }
    }

    /**
     * Get current active DID
     */
    @Nullable
    public String getActiveDid() {
        return activeDid;
    }

    /**
     * Configure TimeSafari integration settings
     * 
     * @param config Configuration options (may include apiServerUrl, did, etc.)
     */
    public void configure(@NonNull org.json.JSONObject config) {
        try {
            logger.d("TS: configure() called");
            
            // Extract and set API server URL if provided
            if (config.has("apiServerUrl")) {
                String url = config.optString("apiServerUrl", null);
                setApiServerUrl(url);
            }
            
            // Extract and set active DID if provided
            if (config.has("did")) {
                String did = config.optString("did", null);
                setActiveDid(did);
            }
            
            logger.i("TS: Configuration applied");
        } catch (Exception e) {
            logger.e("TS: Configuration failed", e);
            throw new RuntimeException("Configuration failed", e);
        }
    }

    /**
     * Update starred plan IDs
     * 
     * Stores the provided plan IDs in SharedPreferences for use by the fetcher.
     * 
     * @param planIds List of plan IDs to star
     */
    public void updateStarredPlans(@NonNull List<String> planIds) {
        try {
            logger.d("TS: updateStarredPlans() called with count=" + planIds.size());
            
            // Validate all plan IDs are non-empty strings
            for (int i = 0; i < planIds.size(); i++) {
                String planId = planIds.get(i);
                if (planId == null || planId.trim().isEmpty()) {
                    throw new IllegalArgumentException("planIds[" + i + "] must be a non-empty string");
                }
            }
            
            // Store in SharedPreferences (matching TestNativeFetcher expectations)
            SharedPreferences preferences = appContext
                .getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE);
            
            // Convert planIds list to JSON array string
            org.json.JSONArray jsonArray = new org.json.JSONArray();
            for (String planId : planIds) {
                jsonArray.put(planId);
            }
            
            preferences.edit()
                .putString("starredPlanIds", jsonArray.toString())
                .apply();
            
            logger.i("TS: Starred plans updated: count=" + planIds.size());
        } catch (Exception e) {
            logger.e("TS: Failed to update starred plans", e);
            throw new RuntimeException("Failed to update starred plans", e);
        }
    }

    /**
     * Handle DID change - clear caches and reschedule
     */
    private void onActiveDidChanged(@Nullable String oldDid, @Nullable String newDid) {
        io.execute(() -> {
            try {
                logger.d("TS: Processing DID swap");
                // Clear per-audience/identity caches, ETags, and any in-memory pagination
                clearCachesForDid(oldDid);
                // Reset JWT (key/claims) for new DID
                if (newDid != null) {
                    jwtManager.setActiveDid(newDid);
                } else {
                    jwtManager.clearAuthentication();
                }
                // Cancel currently scheduled alarms for old DID
                // Note: If notification IDs are scoped by DID, cancel them here
                // For now, cancel all and reschedule (could be optimized)
                scheduler.cancelAllNotifications();
                logger.d("TS: Cleared alarms for old DID");
                
                // Trigger fresh fetch + reschedule for new DID
                if (newDid != null && apiServerUrl != null) {
                    fetchAndScheduleFromServer(true);
                } else {
                    logger.w("TS: Skipping fetch - newDid or apiServerUrl is null");
                }
                
                logger.d("TS: DID swap completed");
            } catch (Exception ex) {
                logger.e("TS: DID swap failed", ex);
            }
        });
    }

    /* ============================================================
     * Fetch & schedule (server → storage → scheduler)
     * ============================================================ */

    /**
     * Pulls notifications from the server and schedules future items.
     * If forceFullSync is true, ignores local pagination windows.
     * 
     * Implementation Notes:
     * - Logic extraction from DailyNotificationPlugin.configureActiveDidIntegration() is planned
     * - Logic extraction from DailyNotificationPlugin scheduling methods is planned
     * - These extractions will be completed as part of future integration refactoring
     * 
     * Note: EnhancedDailyNotificationFetcher returns CompletableFuture<TimeSafariNotificationBundle>
     * Need to convert bundle to NotificationContent[] for storage/scheduling
     */
    public void fetchAndScheduleFromServer(boolean forceFullSync) {
        if (apiServerUrl == null || activeDid == null) {
            logger.w("TS: fetch skipped; apiServerUrl or activeDid is null");
            return;
        }
        
        io.execute(() -> {
            try {
                logger.d("TS: fetchAndScheduleFromServer start forceFullSync=" + forceFullSync);
                
                // 1) Set activeDid for JWT generation
                jwtManager.setActiveDid(activeDid);
                fetcher.setApiServerUrl(apiServerUrl);
                
                // 2) Prepare user config for TimeSafari fetch
                EnhancedDailyNotificationFetcher.TimeSafariUserConfig userConfig = 
                    new EnhancedDailyNotificationFetcher.TimeSafariUserConfig();
                userConfig.activeDid = activeDid;
                userConfig.fetchOffersToPerson = true;
                userConfig.fetchOffersToProjects = true;
                userConfig.fetchProjectUpdates = true;
                
                // Load starred plan IDs from SharedPreferences
                userConfig.starredPlanIds = loadStarredPlanIdsFromSharedPreferences();
                logger.d("TS: Loaded starredPlanIds count=" + 
                         (userConfig.starredPlanIds != null ? userConfig.starredPlanIds.size() : 0));
                
                // 3) Execute fetch (async, but we wait in executor)
                CompletableFuture<EnhancedDailyNotificationFetcher.TimeSafariNotificationBundle> future = 
                    fetcher.fetchAllTimeSafariData(userConfig);
                
                // Wait for result (on background executor, so blocking is OK)
                EnhancedDailyNotificationFetcher.TimeSafariNotificationBundle bundle = 
                    future.get(); // Blocks until complete
                
                if (!bundle.success) {
                    logger.e("TS: Fetch failed: " + (bundle.error != null ? bundle.error : "unknown error"), null);
                    return;
                }
                
                // 4) Convert bundle to NotificationContent[] and save/schedule
                List<NotificationContent> contents = convertBundleToNotificationContent(bundle);
                
                // Get existing notifications for duplicate checking
                java.util.List<NotificationContent> existingNotifications = storage.getAllNotifications();
                long toleranceMs = 60 * 1000; // 1 minute tolerance for DST shifts
                java.util.Set<Long> batchScheduledTimes = new java.util.HashSet<>();
                
                int scheduledCount = 0;
                int skippedCount = 0;
                for (NotificationContent content : contents) {
                    try {
                        // Check for duplicates within current batch
                        long scheduledTime = content.getScheduledTime();
                        boolean duplicateInBatch = false;
                        for (Long batchTime : batchScheduledTimes) {
                            if (Math.abs(batchTime - scheduledTime) <= toleranceMs) {
                                logger.w("TS: DUPLICATE_SKIP_BATCH id=" + content.getId() + 
                                         " scheduled_time=" + scheduledTime);
                                duplicateInBatch = true;
                                skippedCount++;
                                break;
                            }
                        }
                        
                        if (duplicateInBatch) {
                            continue;
                        }
                        
                        // Check for duplicates in existing storage
                        boolean duplicateInStorage = false;
                        for (NotificationContent existing : existingNotifications) {
                            if (Math.abs(existing.getScheduledTime() - scheduledTime) <= toleranceMs) {
                                logger.w("TS: DUPLICATE_SKIP_STORAGE id=" + content.getId() + 
                                         " existing_id=" + existing.getId() + 
                                         " scheduled_time=" + scheduledTime);
                                duplicateInStorage = true;
                                skippedCount++;
                                break;
                            }
                        }
                        
                        if (duplicateInStorage) {
                            continue;
                        }
                        
                        // Mark this scheduledTime as processed
                        batchScheduledTimes.add(scheduledTime);
                        
                        // Save content first
                        storage.saveNotificationContent(content);
                        // TTL validation happens inside scheduler.scheduleNotification()
                        boolean scheduled = scheduler.scheduleNotification(content);
                        if (scheduled) {
                            scheduledCount++;
                        }
                    } catch (Exception perItem) {
                        logger.w("TS: schedule/save failed for id=" + content.getId() + " " + perItem.getMessage());
                    }
                }
                
                logger.i("TS: fetchAndScheduleFromServer done; scheduled=" + scheduledCount + "/" + contents.size() +
                        (skippedCount > 0 ? ", " + skippedCount + " duplicates skipped" : ""));

            } catch (Exception ex) {
                logger.e("TS: fetchAndScheduleFromServer error", ex);
            }
        });
    }

    /**
     * Convert TimeSafariNotificationBundle to NotificationContent list
     * 
     * Converts TimeSafari offers and project updates into NotificationContent objects
     * for scheduling and display.
     */
    private List<NotificationContent> convertBundleToNotificationContent(
            EnhancedDailyNotificationFetcher.TimeSafariNotificationBundle bundle) {
        List<NotificationContent> contents = new java.util.ArrayList<>();
        
        if (bundle == null || !bundle.success) {
            logger.w("TS: Bundle is null or unsuccessful, skipping conversion");
            return contents;
        }
        
        long now = System.currentTimeMillis();
        // Schedule notifications for next morning at 8 AM
        long nextMorning8am = calculateNextMorning8am(now);
        
        try {
            // Convert offers to person
            if (bundle.offersToPerson != null && bundle.offersToPerson.data != null) {
                for (EnhancedDailyNotificationFetcher.OfferSummaryRecord offer : bundle.offersToPerson.data) {
                    NotificationContent content = createOfferNotification(
                        offer, 
                        "offer_person_" + offer.jwtId,
                        "New offer for you",
                        nextMorning8am
                    );
                    if (content != null) {
                        contents.add(content);
                    }
                }
                logger.d("TS: Converted " + bundle.offersToPerson.data.size() + " offers to person");
            }
            
            // Convert offers to projects
            if (bundle.offersToProjects != null && bundle.offersToProjects.data != null && !bundle.offersToProjects.data.isEmpty()) {
                // For now, offersToProjects uses simplified Object structure
                // Create a summary notification if there are any offers
                NotificationContent projectOffersContent = new NotificationContent();
                projectOffersContent.setId("offers_projects_" + now);
                projectOffersContent.setTitle("New offers for your projects");
                projectOffersContent.setBody("You have " + bundle.offersToProjects.data.size() + 
                    " new offer(s) for your projects");
                projectOffersContent.setScheduledTime(nextMorning8am);
                projectOffersContent.setSound(true);
                projectOffersContent.setPriority("default");
                contents.add(projectOffersContent);
                logger.d("TS: Converted " + bundle.offersToProjects.data.size() + " offers to projects");
            }
            
            // Convert project updates
            if (bundle.projectUpdates != null && bundle.projectUpdates.data != null && !bundle.projectUpdates.data.isEmpty()) {
                NotificationContent projectUpdatesContent = new NotificationContent();
                projectUpdatesContent.setId("project_updates_" + now);
                projectUpdatesContent.setTitle("Project updates available");
                projectUpdatesContent.setBody("You have " + bundle.projectUpdates.data.size() + 
                    " project(s) with recent updates");
                projectUpdatesContent.setScheduledTime(nextMorning8am);
                projectUpdatesContent.setSound(true);
                projectUpdatesContent.setPriority("default");
                contents.add(projectUpdatesContent);
                logger.d("TS: Converted " + bundle.projectUpdates.data.size() + " project updates");
            }
            
            logger.i("TS: Total notifications created: " + contents.size());
            
        } catch (Exception e) {
            logger.e("TS: Error converting bundle to notifications", e);
        }
        
        return contents;
    }
    
    /**
     * Create a notification from an offer record
     */
    private NotificationContent createOfferNotification(
            EnhancedDailyNotificationFetcher.OfferSummaryRecord offer,
            String notificationId,
            String defaultTitle,
            long scheduledTime) {
        try {
            if (offer == null || offer.jwtId == null) {
                return null;
            }
            
            NotificationContent content = new NotificationContent();
            content.setId(notificationId);
            
            // Build title from offer details
            String title = defaultTitle;
            if (offer.handleId != null && !offer.handleId.isEmpty()) {
                title = "Offer from @" + offer.handleId;
            }
            content.setTitle(title);
            
            // Build body from offer details
            StringBuilder bodyBuilder = new StringBuilder();
            if (offer.objectDescription != null && !offer.objectDescription.isEmpty()) {
                bodyBuilder.append(offer.objectDescription);
            }
            if (offer.amount > 0 && offer.unit != null) {
                if (bodyBuilder.length() > 0) {
                    bodyBuilder.append(" - ");
                }
                bodyBuilder.append(offer.amount).append(" ").append(offer.unit);
            }
            if (bodyBuilder.length() == 0) {
                bodyBuilder.append("You have a new offer");
            }
            content.setBody(bodyBuilder.toString());
            
            content.setScheduledTime(scheduledTime);
            content.setSound(true);
            content.setPriority("default");
            
            return content;
            
        } catch (Exception e) {
            logger.e("TS: Error creating offer notification", e);
            return null;
        }
    }
    
    /**
     * Calculate next morning at 8 AM
     */
    private long calculateNextMorning8am(long currentTime) {
        try {
            java.util.Calendar calendar = java.util.Calendar.getInstance();
            calendar.setTimeInMillis(currentTime);
            calendar.set(java.util.Calendar.HOUR_OF_DAY, 8);
            calendar.set(java.util.Calendar.MINUTE, 0);
            calendar.set(java.util.Calendar.SECOND, 0);
            calendar.set(java.util.Calendar.MILLISECOND, 0);
            
            // If 8 AM has passed today, schedule for tomorrow
            if (calendar.getTimeInMillis() <= currentTime) {
                calendar.add(java.util.Calendar.DAY_OF_MONTH, 1);
            }
            
            return calendar.getTimeInMillis();
            
        } catch (Exception e) {
            logger.e("TS: Error calculating next morning, using 1 hour from now", e);
            return currentTime + (60 * 60 * 1000); // 1 hour from now as fallback
        }
    }

    /** Force (re)arming of all *future* items from storage—useful after boot or settings change. */
    public void rescheduleAllPending() {
        io.execute(() -> {
            try {
                logger.d("TS: rescheduleAllPending start");
                long now = System.currentTimeMillis();
                List<NotificationContent> allNotifications = storage.getAllNotifications();
                int rescheduledCount = 0;
                
                for (NotificationContent c : allNotifications) {
                    if (c.getScheduledTime() > now) {
                        try {
                            boolean scheduled = scheduler.scheduleNotification(c);
                            if (scheduled) {
                                rescheduledCount++;
                            }
                        } catch (Exception perItem) {
                            logger.w("TS: reschedule failed id=" + c.getId() + " " + perItem.getMessage());
                        }
                    }
                }
                
                logger.i("TS: rescheduleAllPending complete; rescheduled=" + rescheduledCount + "/" + allNotifications.size());
            } catch (Exception ex) {
                logger.e("TS: rescheduleAllPending failed", ex);
            }
        });
    }

    /** Optional: manual refresh hook (dev tools) */
    public void refreshNow() {
        logger.d("TS: refreshNow() triggered");
        fetchAndScheduleFromServer(false);
    }

    /* ============================================================
     * Cache / ETag / Pagination hygiene
     * ============================================================ */

    /**
     * Clear caches for a specific DID
     */
    private void clearCachesForDid(@Nullable String did) {
        try {
            logger.d("TS: clearCachesForDid did=" + (did != null ? did.substring(0, Math.min(20, did.length())) + "..." : "null"));
            
            // Clear ETags that depend on DID/audience
            eTagManager.clearETags();
            
            // Clear notification storage (all content)
            storage.clearAllNotifications();
            
            // Note: EnhancedDailyNotificationFetcher doesn't have resetPagination() method
            // If pagination state needs clearing, add that method
            
            logger.d("TS: clearCachesForDid completed");
        } catch (Exception ex) {
            logger.w("TS: clearCachesForDid encountered issues: " + ex.getMessage());
        }
    }

    /* ============================================================
     * Permissions & channel status aggregation for Plugin.status()
     * ============================================================ */

    /**
     * Get comprehensive status snapshot
     * 
     * Used by plugin's checkStatus() method
     */
    public StatusSnapshot getStatusSnapshot() {
        // Check notification permissions (delegate PIL PermissionManager logic)
        boolean notificationsGranted = false;
        try {
            android.content.pm.PackageManager pm = appContext.getPackageManager();
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
                notificationsGranted = appContext.checkSelfPermission(
                    android.Manifest.permission.POST_NOTIFICATIONS) == 
                    android.content.pm.PackageManager.PERMISSION_GRANTED;
            } else {
                notificationsGranted = androidx.core.app.NotificationManagerCompat
                    .from(appContext).areNotificationsEnabled();
            }
        } catch (Exception e) {
            logger.w("TS: Error checking notification permission: " + e.getMessage());
        }
        
        // Check exact alarm capability
        boolean exactAlarmCapable = false;
        try {
            PendingIntentManager.AlarmStatus alarmStatus = scheduler.getAlarmStatus();
            exactAlarmCapable = alarmStatus.canScheduleNow;
        } catch (Exception e) {
            logger.w("TS: Error checking exact alarm capability: " + e.getMessage());
        }
        
        // Get channel info
        String channelId = channelManager.getDefaultChannelId();
        int channelImportance = channelManager.getChannelImportance();
        
        return new StatusSnapshot(
                notificationsGranted,
                exactAlarmCapable,
                channelId,
                channelImportance,
                activeDid,
                apiServerUrl
        );
    }

    /* ============================================================
     * Teardown (if needed)
     * ============================================================ */

    /**
     * Shutdown and cleanup
     */
    public void shutdown() {
        logger.d("TS: shutdown()");
        // If you replace the Executor with something closeable, do it here
        // For now, single-threaded executor will be GC'd when manager is GC'd
    }
    
    /* ============================================================
     * Helper Methods
     * ============================================================ */
    
    /**
     * Load starred plan IDs from SharedPreferences
     * 
     * Reads the persisted starred plan IDs that were stored via
     * DailyNotificationPlugin.updateStarredPlans()
     * 
     * @return List of starred plan IDs, or empty list if none stored
     */
    @NonNull
    private List<String> loadStarredPlanIdsFromSharedPreferences() {
        try {
            SharedPreferences preferences = appContext
                .getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE);
            
            String starredPlansJson = preferences.getString("starredPlanIds", "[]");
            
            if (starredPlansJson == null || starredPlansJson.isEmpty()) {
                return new ArrayList<>();
            }
            
            JSONArray jsonArray = new JSONArray(starredPlansJson);
            List<String> planIds = new ArrayList<>();
            
            for (int i = 0; i < jsonArray.length(); i++) {
                planIds.add(jsonArray.getString(i));
            }
            
            return planIds;
            
        } catch (JSONException e) {
            logger.e("TS: Error parsing starredPlanIds from SharedPreferences", e);
            return new ArrayList<>();
        } catch (Exception e) {
            logger.e("TS: Unexpected error loading starredPlanIds", e);
            return new ArrayList<>();
        }
    }
}

