/**
 * DailyNotificationFetchWorker.java
 * 
 * WorkManager worker for background content fetching
 * Implements the prefetch step with timeout handling and retry logic
 * 
 * @author Matthew Raymer
 * @version 1.0.0
 */

package org.timesafari.dailynotification;

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

import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.CompletableFuture;
import java.util.Random;

/**
 * Metrics interface for fetch worker operations
 */
interface FetchWorkerMetrics {
    void incRun();
    void incSuccess();
    void incFailure();
    void incRetry();
    void observeDurationMs(long ms);
    void observeItemsEnqueued(int n);
    void observeItemsFetched(int n);
    void observeItemsSaved(int n);
}

/**
 * No-op metrics implementation
 */
final class NoopFetchWorkerMetrics implements FetchWorkerMetrics {
    public void incRun() {}
    public void incSuccess() {}
    public void incFailure() {}
    public void incRetry() {}
    public void observeDurationMs(long ms) {}
    public void observeItemsEnqueued(int n) {}
    public void observeItemsFetched(int n) {}
    public void observeItemsSaved(int n) {}
}

/**
 * Background worker for fetching daily notification content
 * 
 * This worker implements the prefetch step of the offline-first pipeline.
 * It runs in the background to fetch content before it's needed,
 * with proper timeout handling and retry mechanisms.
 */
public class DailyNotificationFetchWorker extends Worker {
    
    private static final String TAG = "DailyNotificationFetchWorker";
    private static final String KEY_SCHEDULED_TIME = "scheduled_time";
    private static final String KEY_FETCH_TIME = "fetch_time";
    private static final String KEY_RETRY_COUNT = "retry_count";
    private static final String KEY_IMMEDIATE = "immediate";
    
    private static final int MAX_RETRY_ATTEMPTS = 3;
    private static final long WORK_TIMEOUT_MS = 8 * 60 * 1000; // 8 minutes total
    
    // Legacy timeout (will be replaced by SchedulingPolicy)
    private static final long FETCH_TIMEOUT_MS_DEFAULT = 30 * 1000; // 30 seconds
    
    private final Context context;
    private final DailyNotificationStorage storage;
    private final DailyNotificationFetcher fetcher; // Legacy fetcher (fallback only)
    private final FetchWorkerMetrics metrics;
    
    /**
     * Constructor
     * 
     * @param context Application context
     * @param params Worker parameters
     */
    public DailyNotificationFetchWorker(@NonNull Context context, 
                                      @NonNull WorkerParameters params) {
        super(context, params);
        this.context = context;
        this.storage = new DailyNotificationStorage(context);
        this.fetcher = new DailyNotificationFetcher(context, storage);
        this.metrics = new NoopFetchWorkerMetrics();
    }
    
    /**
     * Main work method - fetch content with timeout and retry logic
     * 
     * @return Result indicating success, failure, or retry
     */
    @NonNull
    @Override
    public Result doWork() {
        long started = System.currentTimeMillis();
        metrics.incRun();
        
        try {
            Log.d(TAG, "Starting background content fetch");
            
            // Get input data
            Data inputData = getInputData();
            long scheduledTime = inputData.getLong(KEY_SCHEDULED_TIME, 0);
            long fetchTime = inputData.getLong(KEY_FETCH_TIME, 0);
            int retryCount = inputData.getInt(KEY_RETRY_COUNT, 0);
            boolean immediate = inputData.getBoolean(KEY_IMMEDIATE, false);
            
            Log.d(TAG, String.format("PR2: Fetch parameters - Scheduled: %d, Fetch: %d, Retry: %d, Immediate: %s",
                scheduledTime, fetchTime, retryCount, immediate));
            
            // Check if we should proceed with fetch
            if (!shouldProceedWithFetch(scheduledTime, fetchTime)) {
                Log.d(TAG, "Skipping fetch - conditions not met");
                metrics.incSuccess();
                metrics.observeDurationMs(System.currentTimeMillis() - started);
                return Result.success();
            }
            
            // PR2: Attempt to fetch content using native fetcher SPI
            List<NotificationContent> contents = fetchContentWithTimeout(scheduledTime, fetchTime, immediate);
            
            if (contents != null && !contents.isEmpty()) {
                // Success - save contents and schedule notifications
                handleSuccessfulFetch(contents);
                metrics.incSuccess();
                metrics.observeDurationMs(System.currentTimeMillis() - started);
                return Result.success();
                
            } else {
                // Fetch failed - handle retry logic
                Result result = handleFailedFetch(retryCount, scheduledTime);
                metrics.observeDurationMs(System.currentTimeMillis() - started);
                return result;
            }
            
        } catch (Exception e) {
            Log.e(TAG, "Unexpected error during background fetch", e);
            boolean retryable = isRetryable(e);
            if (retryable) {
                metrics.incRetry();
            } else {
                metrics.incFailure();
            }
            metrics.observeDurationMs(System.currentTimeMillis() - started);
            return handleFailedFetch(0, 0);
        }
    }
    
    /**
     * Classify whether an exception is retryable
     * 
     * @param t Exception to classify
     * @return true if retryable, false otherwise
     */
    private boolean isRetryable(Throwable t) {
        if (t == null) return true;

        // Common network-ish failures
        String name = t.getClass().getName();
        if (name.contains("SocketTimeout") || name.contains("ConnectException") || 
            name.contains("UnknownHost") || name.contains("TimeoutException")) {
            return true;
        }

        // If you have HTTP status errors, classify them (adapt to your exception type)
        try {
            java.lang.reflect.Method m = t.getClass().getMethod("getStatusCode");
            Object codeObj = m.invoke(t);
            if (codeObj instanceof Integer) {
                int code = (Integer) codeObj;
                if (code == 429) return true; // Rate limit - retry with backoff
                if (code >= 500) return true; // Server error - retry
                if (code >= 400) return false; // Client error (except 429) - don't retry
            }
        } catch (Exception ignore) {
            // Not an HTTP exception; treat as retryable by default
        }

        return true;
    }
    
    /**
     * Check if we should proceed with the fetch
     * 
     * @param scheduledTime When notification is scheduled for
     * @param fetchTime When fetch was originally scheduled for
     * @return true if fetch should proceed
     */
    private boolean shouldProceedWithFetch(long scheduledTime, long fetchTime) {
        long currentTime = System.currentTimeMillis();
        
        // If this is an immediate fetch, always proceed
        if (fetchTime == 0) {
            return true;
        }
        
        // Check if fetch time has passed
        if (currentTime < fetchTime) {
            Log.d(TAG, "Fetch time not yet reached");
            return false;
        }
        
        // Check if notification time has passed
        if (currentTime >= scheduledTime) {
            Log.d(TAG, "Notification time has passed, fetch not needed");
            return false;
        }
        
        // Check if we already have recent content
        if (!storage.shouldFetchNewContent()) {
            Log.d(TAG, "Recent content available, fetch not needed");
            return false;
        }
        
        return true;
    }
    
    /**
     * Fetch content with timeout handling using native fetcher SPI (PR2)
     * 
     * @param scheduledTime When notification is scheduled for
     * @param fetchTime When fetch was triggered
     * @param immediate Whether this is an immediate fetch
     * @return List of fetched notification contents or null if failed
     */
    private List<NotificationContent> fetchContentWithTimeout(long scheduledTime, long fetchTime, boolean immediate) {
        try {
            // Get SchedulingPolicy for timeout configuration
            SchedulingPolicy policy = getSchedulingPolicy();
            long fetchTimeoutMs = policy.fetchTimeoutMs != null ? 
                policy.fetchTimeoutMs : FETCH_TIMEOUT_MS_DEFAULT;
            
            Log.d(TAG, "PR2: Fetching content with native fetcher SPI, timeout: " + fetchTimeoutMs + "ms");
            
            // Get native fetcher from static registry
            NativeNotificationContentFetcher nativeFetcher = DailyNotificationPlugin.getNativeFetcherStatic();
            
            if (nativeFetcher == null) {
                Log.w(TAG, "PR2: Native fetcher not registered, falling back to legacy fetcher");
                // Fallback to legacy fetcher
                NotificationContent content = fetcher.fetchContentImmediately();
                if (content != null) {
                    return java.util.Collections.singletonList(content);
                }
                return null;
            }
            
            long startTime = System.currentTimeMillis();
            
            // Create FetchContext
            String trigger = immediate ? "manual" : 
                (fetchTime > 0 ? "prefetch" : "background_work");
            Long scheduledTimeOpt = scheduledTime > 0 ? scheduledTime : null;
            Map<String, Object> metadata = new HashMap<>();
            metadata.put("retryCount", 0);
            metadata.put("immediate", immediate);
            
            FetchContext context = new FetchContext(
                trigger,
                scheduledTimeOpt,
                fetchTime > 0 ? fetchTime : System.currentTimeMillis(),
                metadata
            );
            
            // Call native fetcher with timeout
            CompletableFuture<List<NotificationContent>> future = nativeFetcher.fetchContent(context);
            
            List<NotificationContent> contents;
            try {
                contents = future.get(fetchTimeoutMs, TimeUnit.MILLISECONDS);
            } catch (TimeoutException e) {
                Log.e(TAG, "PR2: Native fetcher timeout after " + fetchTimeoutMs + "ms", e);
                return null;
            }
            
            long fetchDuration = System.currentTimeMillis() - startTime;
            
            if (contents != null && !contents.isEmpty()) {
                Log.i(TAG, "PR2: Content fetched successfully - " + contents.size() + 
                    " items in " + fetchDuration + "ms");
                metrics.observeItemsFetched(contents.size());
                return contents;
            } else {
                Log.w(TAG, "PR2: Native fetcher returned empty list after " + fetchDuration + "ms");
                metrics.incFailure();
                return null;
            }
            
        } catch (Exception e) {
            Log.e(TAG, "PR2: Error during native fetcher call", e);
            boolean retryable = isRetryable(e);
            if (retryable) {
                metrics.incRetry();
            } else {
                metrics.incFailure();
            }
            return null;
        }
    }
    
    /**
     * Get SchedulingPolicy from SharedPreferences or return default
     * 
     * @return SchedulingPolicy instance
     */
    private SchedulingPolicy getSchedulingPolicy() {
        try {
            // Try to load from SharedPreferences (set via plugin's setPolicy method)
            android.content.SharedPreferences prefs = context.getSharedPreferences(
                "daily_notification_spi", Context.MODE_PRIVATE);
            
            // NOTE: We intentionally do not deserialize large payloads from SharedPreferences.
            // Storage of notification content is handled by DailyNotificationStorage/DB layer.
            // SchedulingPolicy is lightweight and can be stored in SharedPreferences if needed in future.
            return SchedulingPolicy.createDefault();
            
        } catch (Exception e) {
            Log.w(TAG, "Error loading SchedulingPolicy, using default", e);
            return SchedulingPolicy.createDefault();
        }
    }
    
    /**
     * Handle successful content fetch (PR2: now handles List<NotificationContent>)
     * 
     * @param contents List of successfully fetched notification contents
     */
    private void handleSuccessfulFetch(List<NotificationContent> contents) {
        try {
            Log.d(TAG, "PR2: Handling successful content fetch - " + contents.size() + " items");
            
            // Update last fetch time
            storage.setLastFetchTime(System.currentTimeMillis());
            
            // Get existing notifications for duplicate checking (prevent prefetch from creating duplicate)
            java.util.List<NotificationContent> existingNotifications = storage.getAllNotifications();
            long toleranceMs = 60 * 1000; // 1 minute tolerance for DST shifts
            
            // Track scheduled times in current batch to prevent within-batch duplicates
            java.util.Set<Long> batchScheduledTimes = new java.util.HashSet<>();
            
            // Save all contents and schedule notifications (with duplicate checking)
            int scheduledCount = 0;
            int skippedCount = 0;
            for (NotificationContent content : contents) {
                try {
                    // Check for duplicate notification at the same scheduled time
                    // First check within current batch (prevents duplicates in same fetch)
                    long scheduledTime = content.getScheduledTime();
                    boolean duplicateInBatch = false;
                    for (Long batchTime : batchScheduledTimes) {
                        if (Math.abs(batchTime - scheduledTime) <= toleranceMs) {
                            Log.w(TAG, "PR2: DUPLICATE_SKIP_BATCH id=" + content.getId() + 
                                      " scheduled_time=" + scheduledTime +
                                      " time_diff_ms=" + Math.abs(batchTime - scheduledTime));
                            duplicateInBatch = true;
                            skippedCount++;
                            break;
                        }
                    }
                    
                    if (duplicateInBatch) {
                        continue;
                    }
                    
                    // Then check against existing notifications in storage
                    boolean duplicateInStorage = false;
                    for (NotificationContent existing : existingNotifications) {
                        if (Math.abs(existing.getScheduledTime() - scheduledTime) <= toleranceMs) {
                            Log.w(TAG, "PR2: DUPLICATE_SKIP_STORAGE id=" + content.getId() + 
                                      " existing_id=" + existing.getId() + 
                                      " scheduled_time=" + scheduledTime +
                                      " time_diff_ms=" + Math.abs(existing.getScheduledTime() - scheduledTime));
                            duplicateInStorage = true;
                            skippedCount++;
                            break;
                        }
                    }
                    
                    if (duplicateInStorage) {
                        // Skip this notification - one already exists for this time
                        continue;
                    }
                    
                    // Mark this scheduledTime as processed in current batch
                    batchScheduledTimes.add(scheduledTime);
                    
                    // Save content to storage
                    storage.saveNotificationContent(content);
            
            // Schedule notification if not already scheduled
            scheduleNotificationIfNeeded(content);
                    scheduledCount++;
                    
                } catch (Exception e) {
                    Log.e(TAG, "PR2: Error processing notification content: " + content.getId(), e);
                }
            }
            
            Log.i(TAG, "PR2: Successful fetch handling completed - " + scheduledCount + "/" + 
                contents.size() + " notifications scheduled" + 
                (skippedCount > 0 ? ", " + skippedCount + " duplicates skipped" : ""));
            
            // Record metrics
            metrics.observeItemsFetched(contents.size());
            metrics.observeItemsSaved(scheduledCount);
            metrics.observeItemsEnqueued(scheduledCount);
            
        } catch (Exception e) {
            Log.e(TAG, "PR2: Error handling successful fetch", e);
        }
    }
    
    /**
     * Handle failed content fetch with retry logic
     * 
     * @param retryCount Current retry attempt
     * @param scheduledTime When notification is scheduled for
     * @return Result indicating retry or failure
     */
    private Result handleFailedFetch(int retryCount, long scheduledTime) {
        try {
            Log.d(TAG, "PR2: Handling failed fetch - Retry: " + retryCount);
            
            if (retryCount < MAX_RETRY_ATTEMPTS) {
                // PR2: Schedule retry with SchedulingPolicy backoff
                scheduleRetry(retryCount + 1, scheduledTime);
                Log.i(TAG, "PR2: Scheduled retry attempt " + (retryCount + 1));
                metrics.incRetry();
                return Result.retry();
                
            } else {
                // Max retries reached - use fallback content
                Log.w(TAG, "PR2: Max retries reached, using fallback content");
                useFallbackContent(scheduledTime);
                metrics.incFailure();
                return Result.success();
            }
            
        } catch (Exception e) {
            Log.e(TAG, "PR2: Error handling failed fetch", e);
            boolean retryable = isRetryable(e);
            if (retryable) {
                metrics.incRetry();
            } else {
                metrics.incFailure();
            }
            return Result.failure();
        }
    }
    
    /**
     * Schedule a retry attempt
     * 
     * @param retryCount New retry attempt number
     * @param scheduledTime When notification is scheduled for
     */
    private void scheduleRetry(int retryCount, long scheduledTime) {
        try {
            Log.d(TAG, "Scheduling retry attempt " + retryCount);
            
            // Calculate retry delay with exponential backoff
            long retryDelay = calculateRetryDelay(retryCount);
            
            // Create retry work request
            Data retryData = new Data.Builder()
                .putLong(KEY_SCHEDULED_TIME, scheduledTime)
                .putLong(KEY_FETCH_TIME, System.currentTimeMillis())
                .putInt(KEY_RETRY_COUNT, retryCount)
                .build();
            
            androidx.work.OneTimeWorkRequest retryWork = 
                new androidx.work.OneTimeWorkRequest.Builder(DailyNotificationFetchWorker.class)
                    .setInputData(retryData)
                    .setInitialDelay(retryDelay, TimeUnit.MILLISECONDS)
                    .build();
            
            androidx.work.WorkManager.getInstance(context).enqueue(retryWork);
            
            Log.d(TAG, "Retry scheduled for " + retryDelay + "ms from now");
            
        } catch (Exception e) {
            Log.e(TAG, "Error scheduling retry", e);
        }
    }
    
    /**
     * Calculate retry delay with exponential backoff using SchedulingPolicy (PR2)
     * 
     * @param retryCount Current retry attempt
     * @return Delay in milliseconds
     */
    private long calculateRetryDelay(int retryCount) {
        SchedulingPolicy policy = getSchedulingPolicy();
        SchedulingPolicy.RetryBackoff backoff = policy.retryBackoff;
        
        // Calculate exponential delay: minMs * (factor ^ (retryCount - 1))
        long baseDelay = backoff.minMs;
        double exponentialMultiplier = Math.pow(backoff.factor, retryCount - 1);
        long exponentialDelay = (long) (baseDelay * exponentialMultiplier);
            
        // Cap at maxMs
        long cappedDelay = Math.min(exponentialDelay, backoff.maxMs);
        
        // Add jitter: delay * (1 + jitterPct/100 * random(0-1))
        Random random = new Random();
        double jitter = backoff.jitterPct / 100.0 * random.nextDouble();
        long finalDelay = (long) (cappedDelay * (1.0 + jitter));
        
        Log.d(TAG, "PR2: Calculated retry delay - attempt=" + retryCount + 
            ", base=" + baseDelay + "ms, exponential=" + exponentialDelay + "ms, " +
            "capped=" + cappedDelay + "ms, jitter=" + String.format("%.1f%%", jitter * 100) + 
            ", final=" + finalDelay + "ms");
        
        return finalDelay;
    }
    
    /**
     * Use fallback content when all retries fail
     * 
     * @param scheduledTime When notification is scheduled for
     */
    private void useFallbackContent(long scheduledTime) {
        try {
            Log.d(TAG, "Using fallback content for scheduled time: " + scheduledTime);
            
            // Get fallback content from storage or create emergency content
            NotificationContent fallbackContent = getFallbackContent(scheduledTime);
            
            if (fallbackContent != null) {
                // Save fallback content
                storage.saveNotificationContent(fallbackContent);
                
                // Schedule notification
                scheduleNotificationIfNeeded(fallbackContent);
                
                Log.i(TAG, "Fallback content applied successfully");
            } else {
                Log.e(TAG, "Failed to get fallback content");
            }
            
        } catch (Exception e) {
            Log.e(TAG, "Error using fallback content", e);
        }
    }
    
    /**
     * Get fallback content for the scheduled time
     * 
     * @param scheduledTime When notification is scheduled for
     * @return Fallback notification content
     */
    private NotificationContent getFallbackContent(long scheduledTime) {
        try {
            // Try to get last known good content
            NotificationContent lastContent = storage.getLastNotification();
            
            if (lastContent != null && !lastContent.isStale()) {
                Log.d(TAG, "Using last known good content as fallback");
                
                // Create new content based on last good content
                NotificationContent fallbackContent = new NotificationContent();
                fallbackContent.setTitle(lastContent.getTitle());
                fallbackContent.setBody(lastContent.getBody() + " (from " + 
                    lastContent.getAgeString() + ")");
                fallbackContent.setScheduledTime(scheduledTime);
                fallbackContent.setSound(lastContent.isSound());
                fallbackContent.setPriority(lastContent.getPriority());
                fallbackContent.setUrl(lastContent.getUrl());
                // fetchedAt is set in constructor, no need to set it again
                
                return fallbackContent;
            }
            
            // Create emergency fallback content
            Log.w(TAG, "Creating emergency fallback content");
            return createEmergencyFallbackContent(scheduledTime);
            
        } catch (Exception e) {
            Log.e(TAG, "Error getting fallback content", e);
            return createEmergencyFallbackContent(scheduledTime);
        }
    }
    
    /**
     * Create emergency fallback content
     * 
     * @param scheduledTime When notification is scheduled for
     * @return Emergency notification content
     */
    private NotificationContent createEmergencyFallbackContent(long scheduledTime) {
        NotificationContent content = new NotificationContent();
        content.setTitle("Daily Update");
        content.setBody("🌅 Good morning! Ready to make today amazing?");
        content.setScheduledTime(scheduledTime);
        // fetchedAt is set in constructor, no need to set it again
        content.setPriority("default");
        content.setSound(true);
        
        return content;
    }
    
    /**
     * Schedule notification if not already scheduled
     * 
     * @param content Notification content to schedule
     */
    private void scheduleNotificationIfNeeded(NotificationContent content) {
        try {
            Log.d(TAG, "Checking if notification needs scheduling: " + content.getId());
            
            // Check if notification is already scheduled
            DailyNotificationScheduler scheduler = new DailyNotificationScheduler(
                context,
                (android.app.AlarmManager) context.getSystemService(Context.ALARM_SERVICE)
            );
            
            if (!scheduler.isNotificationScheduled(content.getId())) {
                Log.d(TAG, "Scheduling notification: " + content.getId());
                boolean scheduled = scheduler.scheduleNotification(content);
                
                if (scheduled) {
                    Log.i(TAG, "Notification scheduled successfully");
                } else {
                    Log.e(TAG, "Failed to schedule notification");
                }
            } else {
                Log.d(TAG, "Notification already scheduled: " + content.getId());
            }
            
        } catch (Exception e) {
            Log.e(TAG, "Error checking/scheduling notification", e);
        }
    }
}
