/**
 * TestNativeFetcher.java
 * 
 * Test implementation of NativeNotificationContentFetcher for the test app.
 * Fetches real notification content from the endorser API endpoint.
 * 
 * @author Matthew Raymer
 * @version 1.0.0
 */

package org.timesafari.dailynotification.test;

import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.NonNull;
import org.timesafari.dailynotification.FetchContext;
import org.timesafari.dailynotification.NativeNotificationContentFetcher;
import org.timesafari.dailynotification.NotificationContent;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonArray;
import com.google.gson.JsonParser;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

/**
 * Test implementation of native content fetcher
 * 
 * Fetches real notification content from the endorser API endpoint.
 * Uses http://10.0.2.2:3000 for Android emulator (maps to host localhost:3000).
 */
public class TestNativeFetcher implements NativeNotificationContentFetcher {
    
    private static final String TAG = "TestNativeFetcher";
    private static final String ENDORSER_ENDPOINT = "/api/v2/report/plansLastUpdatedBetween";
    private static final int CONNECT_TIMEOUT_MS = 10000; // 10 seconds
    private static final int READ_TIMEOUT_MS = 15000;    // 15 seconds
    private static final int MAX_RETRIES = 3; // Maximum number of retry attempts
    private static final int RETRY_DELAY_MS = 1000; // Base delay for exponential backoff
    
    // SharedPreferences constants
    // NOTE: Must match plugin's SharedPreferences name and keys for starred plans
    // Plugin uses "daily_notification_timesafari" (see DailyNotificationPlugin.updateStarredPlans)
    private static final String PREFS_NAME = "daily_notification_timesafari";
    private static final String KEY_STARRED_PLAN_IDS = "starredPlanIds";  // Matches plugin key
    private static final String KEY_LAST_ACKED_JWT_ID = "last_acked_jwt_id";  // Plugin doesn't use this yet
    
    private final Gson gson = new Gson();
    private final Context appContext;
    private SharedPreferences prefs;
    
    // Volatile fields for configuration, set via configure() method
    private volatile String apiBaseUrl;
    private volatile String activeDid;
    private volatile String jwtToken; // Pre-generated JWT token from TypeScript (ES256K signed)
    
    /**
     * Constructor
     * 
     * @param context Application context for SharedPreferences access
     */
    public TestNativeFetcher(Context context) {
        this.appContext = context.getApplicationContext();
        this.prefs = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
        Log.d(TAG, "TestNativeFetcher: Initialized with context");
    }
    
    /**
     * Configure the native fetcher with API credentials
     * 
     * Called by the plugin when configureNativeFetcher() is invoked from TypeScript.
     * This method stores the configuration for use in background fetches.
     * 
     * <p><b>Architecture Note:</b> The JWT token is pre-generated in TypeScript using
     * TimeSafari's {@code createEndorserJwtForKey()} function (ES256K DID-based signing).
     * The native fetcher just uses the token directly - no JWT generation needed.</p>
     * 
     * @param apiBaseUrl Base URL for API server (e.g., "http://10.0.2.2:3000")
     * @param activeDid Active DID for authentication (e.g., "did:ethr:0x...")
     * @param jwtToken Pre-generated JWT token (ES256K signed) from TypeScript
     */
    @Override
    public void configure(String apiBaseUrl, String activeDid, String jwtToken) {
        configure(apiBaseUrl, activeDid, jwtToken, null);
    }

    @Override
    public void configure(String apiBaseUrl, String activeDid, String jwtToken, @Nullable List<String> jwtTokenPool) {
        this.apiBaseUrl = apiBaseUrl;
        this.activeDid = activeDid;
        this.jwtToken = jwtToken;
        if (jwtTokenPool != null && !jwtTokenPool.isEmpty()) {
            Log.i(TAG, "TestNativeFetcher: jwtTokenPool size=" + jwtTokenPool.size());
        }
        
        // Enhanced logging for JWT diagnostic purposes
        Log.i(TAG, "TestNativeFetcher: Configured with API: " + apiBaseUrl);
        if (activeDid != null) {
            Log.i(TAG, "TestNativeFetcher: ActiveDID: " + activeDid.substring(0, Math.min(30, activeDid.length())) + 
                       (activeDid.length() > 30 ? "..." : ""));
        } else {
            Log.w(TAG, "TestNativeFetcher: ActiveDID is NULL");
        }
        
        if (jwtToken != null) {
            Log.i(TAG, "TestNativeFetcher: JWT token received - Length: " + jwtToken.length() + " chars");
            // Log first and last 10 chars for verification (not full token for security)
            String tokenPreview = jwtToken.length() > 20 
                ? jwtToken.substring(0, 10) + "..." + jwtToken.substring(jwtToken.length() - 10)
                : jwtToken.substring(0, Math.min(jwtToken.length(), 20)) + "...";
            Log.d(TAG, "TestNativeFetcher: JWT preview: " + tokenPreview);
        } else {
            Log.e(TAG, "TestNativeFetcher: JWT token is NULL - API calls will fail");
        }
    }
    
    @Override
    @NonNull
    public CompletableFuture<List<NotificationContent>> fetchContent(
            @NonNull FetchContext context) {
        
        Log.d(TAG, "TestNativeFetcher: Fetch triggered - trigger=" + context.trigger +
            ", scheduledTime=" + context.scheduledTime + ", fetchTime=" + context.fetchTime);
        
        // Start with retry count 0
        return fetchContentWithRetry(context, 0);
    }
    
    /**
     * Fetch content with retry logic for transient errors
     * 
     * @param context Fetch context
     * @param retryCount Current retry attempt (0 for first attempt)
     * @return Future with notification contents or empty list on failure
     */
    private CompletableFuture<List<NotificationContent>> fetchContentWithRetry(
            @NonNull FetchContext context, int retryCount) {
        
        return CompletableFuture.supplyAsync(() -> {
            try {
                // Check if configured
                if (apiBaseUrl == null || activeDid == null || jwtToken == null) {
                    Log.e(TAG, "TestNativeFetcher: Not configured. Call configureNativeFetcher() from TypeScript first.");
                    return Collections.emptyList();
                }
                
                Log.i(TAG, "TestNativeFetcher: Starting fetch from " + apiBaseUrl + ENDORSER_ENDPOINT);
                
                // Build request URL
                String urlString = apiBaseUrl + ENDORSER_ENDPOINT;
                URL url = new URL(urlString);
                
                // Create HTTP connection
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
                connection.setReadTimeout(READ_TIMEOUT_MS);
                connection.setRequestMethod("POST");
                connection.setRequestProperty("Content-Type", "application/json");
                
                // Diagnostic logging for JWT usage
                if (jwtToken != null) {
                    String jwtPreview = jwtToken.length() > 20 
                        ? jwtToken.substring(0, 10) + "..." + jwtToken.substring(jwtToken.length() - 10)
                        : jwtToken;
                    Log.d(TAG, "TestNativeFetcher: Using JWT for API call - Length: " + jwtToken.length() + 
                               ", Preview: " + jwtPreview + ", ActiveDID: " + 
                               (activeDid != null ? activeDid.substring(0, Math.min(30, activeDid.length())) + "..." : "null"));
                } else {
                    Log.e(TAG, "TestNativeFetcher: JWT token is NULL when making API call!");
                }
                
                connection.setRequestProperty("Authorization", "Bearer " + jwtToken);
                connection.setDoOutput(true);
                
                // Build request body
                Map<String, Object> requestBody = new HashMap<>();
                requestBody.put("planIds", getStarredPlanIds());
                
                // afterId is required by the API endpoint
                // Use "0" for first request (no previous data), or stored jwtId for subsequent requests
                String afterId = getLastAcknowledgedJwtId();
                if (afterId == null || afterId.isEmpty()) {
                    afterId = "0"; // First request - start from beginning
                }
                requestBody.put("afterId", afterId);
                
                String jsonBody = gson.toJson(requestBody);
                Log.d(TAG, "TestNativeFetcher: Request body: " + jsonBody);
                
                // Write request body
                try (OutputStream os = connection.getOutputStream()) {
                    byte[] input = jsonBody.getBytes(StandardCharsets.UTF_8);
                    os.write(input, 0, input.length);
                }
                
                // Execute request
                int responseCode = connection.getResponseCode();
                Log.d(TAG, "TestNativeFetcher: HTTP response code: " + responseCode);
                
                if (responseCode == 200) {
                    // Read response
                    BufferedReader reader = new BufferedReader(
                        new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
                    StringBuilder response = new StringBuilder();
                    String line;
                    while ((line = reader.readLine()) != null) {
                        response.append(line);
                    }
                    reader.close();
                    
                    String responseBody = response.toString();
                    Log.d(TAG, "TestNativeFetcher: Response body length: " + responseBody.length());
                    
                    // Parse response and convert to NotificationContent
                    List<NotificationContent> contents = parseApiResponse(responseBody, context);
                    
                    // Update last acknowledged JWT ID from the response (for pagination)
                    if (!contents.isEmpty()) {
                        // Get the last JWT ID from the parsed response (stored during parsing)
                        updateLastAckedJwtIdFromResponse(contents, responseBody);
                    }
                    
                    Log.i(TAG, "TestNativeFetcher: Successfully fetched " + contents.size() + 
                        " notification(s)");
                    
                    return contents;
                    
                } else {
                    // Read error response
                    String errorMessage = "Unknown error";
                    try {
                        BufferedReader reader = new BufferedReader(
                            new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8));
                        StringBuilder errorResponse = new StringBuilder();
                        String line;
                        while ((line = reader.readLine()) != null) {
                            errorResponse.append(line);
                        }
                        reader.close();
                        errorMessage = errorResponse.toString();
                    } catch (Exception e) {
                        Log.w(TAG, "TestNativeFetcher: Could not read error stream", e);
                    }
                    
                    Log.e(TAG, "TestNativeFetcher: API error " + responseCode + ": " + errorMessage);
                    
                    // Handle retryable errors (5xx server errors, network timeouts)
                    if (shouldRetry(responseCode, retryCount)) {
                        long delayMs = RETRY_DELAY_MS * (1 << retryCount); // Exponential backoff
                        Log.w(TAG, "TestNativeFetcher: Retryable error, retrying in " + delayMs + "ms " +
                                  "(" + (retryCount + 1) + "/" + MAX_RETRIES + ")");
                        
                        try {
                            Thread.sleep(delayMs);
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                            Log.e(TAG, "TestNativeFetcher: Retry delay interrupted", e);
                            return Collections.emptyList();
                        }
                        
                        // Recursive retry
                        return fetchContentWithRetry(context, retryCount + 1).join();
                    }
                    
                    // Non-retryable errors (4xx client errors, max retries reached)
                    if (responseCode >= 400 && responseCode < 500) {
                        Log.e(TAG, "TestNativeFetcher: Non-retryable client error " + responseCode);
                    } else if (retryCount >= MAX_RETRIES) {
                        Log.e(TAG, "TestNativeFetcher: Max retries (" + MAX_RETRIES + ") reached");
                    }
                    
                    // Return empty list on error (fallback will be handled by worker)
                    return Collections.emptyList();
                }
                
            } catch (java.net.SocketTimeoutException | java.net.UnknownHostException e) {
                // Network errors are retryable
                Log.w(TAG, "TestNativeFetcher: Network error during fetch", e);
                
                if (shouldRetry(0, retryCount)) { // Use 0 as response code for network errors
                    long delayMs = RETRY_DELAY_MS * (1 << retryCount);
                    Log.w(TAG, "TestNativeFetcher: Retrying after network error in " + delayMs + "ms " +
                              "(" + (retryCount + 1) + "/" + MAX_RETRIES + ")");
                    
                    try {
                        Thread.sleep(delayMs);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        Log.e(TAG, "TestNativeFetcher: Retry delay interrupted", ie);
                        return Collections.emptyList();
                    }
                    
                    return fetchContentWithRetry(context, retryCount + 1).join();
                }
                
                Log.e(TAG, "TestNativeFetcher: Max retries reached for network error");
                return Collections.emptyList();
                
            } catch (Exception e) {
                Log.e(TAG, "TestNativeFetcher: Error during fetch", e);
                // Non-retryable errors (parsing, configuration, etc.)
                return Collections.emptyList();
            }
        });
    }
    
    /**
     * Determine if an error should be retried
     * 
     * @param responseCode HTTP response code (0 for network errors)
     * @param retryCount Current retry attempt count
     * @return true if error is retryable and retry count not exceeded
     */
    private boolean shouldRetry(int responseCode, int retryCount) {
        if (retryCount >= MAX_RETRIES) {
            return false; // Max retries exceeded
        }
        
        // Retry on network errors (responseCode 0) or server errors (5xx)
        // Don't retry on client errors (4xx) as they indicate permanent issues
        if (responseCode == 0) {
            return true; // Network error (timeout, unknown host, etc.)
        }
        
        if (responseCode >= 500 && responseCode < 600) {
            return true; // Server error (retryable)
        }
        
        // Some 4xx errors might be retryable (e.g., 429 Too Many Requests)
        if (responseCode == 429) {
            return true; // Rate limit - retry with backoff
        }
        
        return false; // Other client errors (401, 403, 404, etc.) are not retryable
    }
    
    /**
     * Get starred plan IDs from SharedPreferences
     * 
     * @return List of starred plan IDs, empty list if none stored
     */
    private List<String> getStarredPlanIds() {
        try {
            // Use the same SharedPreferences as the plugin (not the instance variable 'prefs')
            // Plugin stores in "daily_notification_timesafari" with key "starredPlanIds"
            SharedPreferences pluginPrefs = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
            String idsJson = pluginPrefs.getString(KEY_STARRED_PLAN_IDS, "[]");
            
            if (idsJson == null || idsJson.isEmpty() || idsJson.equals("[]")) {
                Log.d(TAG, "TestNativeFetcher: No starred plan IDs found in SharedPreferences");
                return new ArrayList<>();
            }
            
            // Parse JSON array (plugin stores as JSON string)
            JsonParser parser = new JsonParser();
            JsonArray jsonArray = parser.parse(idsJson).getAsJsonArray();
            List<String> planIds = new ArrayList<>();
            
            for (int i = 0; i < jsonArray.size(); i++) {
                planIds.add(jsonArray.get(i).getAsString());
            }
            
            Log.i(TAG, "TestNativeFetcher: Loaded " + planIds.size() + " starred plan IDs from SharedPreferences");
            if (planIds.size() > 0) {
                Log.d(TAG, "TestNativeFetcher: First plan ID: " + 
                      planIds.get(0).substring(0, Math.min(30, planIds.get(0).length())) + "...");
            }
            return planIds;
            
        } catch (Exception e) {
            Log.e(TAG, "TestNativeFetcher: Error loading starred plan IDs from SharedPreferences", e);
            return new ArrayList<>();
        }
    }
    
    /**
     * Get last acknowledged JWT ID from SharedPreferences (for pagination)
     * 
     * @return Last acknowledged JWT ID, or null if none stored
     */
    private String getLastAcknowledgedJwtId() {
        try {
            String jwtId = prefs.getString(KEY_LAST_ACKED_JWT_ID, null);
            if (jwtId != null) {
                Log.d(TAG, "TestNativeFetcher: Loaded last acknowledged JWT ID");
            }
            return jwtId;
        } catch (Exception e) {
            Log.e(TAG, "TestNativeFetcher: Error loading last acknowledged JWT ID", e);
            return null;
        }
    }
    
    /**
     * Update starred plan IDs in SharedPreferences
     * 
     * @param planIds List of plan IDs to store
     */
    public void updateStarredPlanIds(List<String> planIds) {
        try {
            String idsJson = gson.toJson(planIds);
            prefs.edit().putString(KEY_STARRED_PLAN_IDS, idsJson).apply();
            Log.i(TAG, "TestNativeFetcher: Updated starred plan IDs: " + planIds.size() + " plans");
        } catch (Exception e) {
            Log.e(TAG, "TestNativeFetcher: Error updating starred plan IDs", e);
        }
    }
    
    /**
     * Update last acknowledged JWT ID in SharedPreferences
     * 
     * @param jwtId JWT ID to store as last acknowledged
     */
    public void updateLastAckedJwtId(String jwtId) {
        try {
            prefs.edit().putString(KEY_LAST_ACKED_JWT_ID, jwtId).apply();
            Log.d(TAG, "TestNativeFetcher: Updated last acknowledged JWT ID");
        } catch (Exception e) {
            Log.e(TAG, "TestNativeFetcher: Error updating last acknowledged JWT ID", e);
        }
    }
    
    /**
     * Update last acknowledged JWT ID from the API response
     * Uses the last JWT ID from the data array for pagination
     * 
     * @param contents Parsed notification contents (may contain JWT IDs)
     * @param responseBody Original response body for parsing
     */
    private void updateLastAckedJwtIdFromResponse(List<NotificationContent> contents, String responseBody) {
        try {
            JsonParser parser = new JsonParser();
            JsonObject root = parser.parse(responseBody).getAsJsonObject();
            JsonArray dataArray = root.getAsJsonArray("data");
            
            if (dataArray != null && dataArray.size() > 0) {
                // Get the last item's JWT ID (most recent)
                JsonObject lastItem = dataArray.get(dataArray.size() - 1).getAsJsonObject();
                
                // Try to get JWT ID from different possible locations in response structure
                String jwtId = null;
                if (lastItem.has("jwtId")) {
                    jwtId = lastItem.get("jwtId").getAsString();
                } else if (lastItem.has("plan")) {
                    JsonObject plan = lastItem.getAsJsonObject("plan");
                    if (plan.has("jwtId")) {
                        jwtId = plan.get("jwtId").getAsString();
                    }
                }
                
                if (jwtId != null && !jwtId.isEmpty()) {
                    updateLastAckedJwtId(jwtId);
                    Log.d(TAG, "TestNativeFetcher: Updated last acknowledged JWT ID: " + 
                              jwtId.substring(0, Math.min(20, jwtId.length())) + "...");
                }
            }
        } catch (Exception e) {
            Log.w(TAG, "TestNativeFetcher: Could not extract JWT ID from response for pagination", e);
        }
    }
    
    /**
     * Parse API response and convert to NotificationContent list
     */
    private List<NotificationContent> parseApiResponse(String responseBody, FetchContext context) {
        List<NotificationContent> contents = new ArrayList<>();
        
        try {
            JsonParser parser = new JsonParser();
            JsonObject root = parser.parse(responseBody).getAsJsonObject();
            
            // Parse response structure (matches PlansLastUpdatedResponse)
            JsonArray dataArray = root.getAsJsonArray("data");
            if (dataArray != null) {
                for (int i = 0; i < dataArray.size(); i++) {
                    JsonObject item = dataArray.get(i).getAsJsonObject();
                    
                    NotificationContent content = new NotificationContent();
                    
                    // Extract data from API response
                    // Support both flat structure (jwtId, planId) and nested (plan.jwtId, plan.handleId)
                    String planId = null;
                    String jwtId = null;
                    
                    if (item.has("planId")) {
                        planId = item.get("planId").getAsString();
                    } else if (item.has("plan")) {
                        JsonObject plan = item.getAsJsonObject("plan");
                        if (plan.has("handleId")) {
                            planId = plan.get("handleId").getAsString();
                        }
                    }
                    
                    if (item.has("jwtId")) {
                        jwtId = item.get("jwtId").getAsString();
                    } else if (item.has("plan")) {
                        JsonObject plan = item.getAsJsonObject("plan");
                        if (plan.has("jwtId")) {
                            jwtId = plan.get("jwtId").getAsString();
                        }
                    }
                    
                    // Create notification ID
                    String notificationId = "endorser_" + (jwtId != null ? jwtId : 
                        System.currentTimeMillis() + "_" + i);
                    content.setId(notificationId);
                    
                    // Create notification title
                    String title = "Project Update";
                    if (planId != null) {
                        title = "Update: " + planId.substring(Math.max(0, planId.length() - 8));
                    }
                    content.setTitle(title);
                    
                    // Create notification body
                    StringBuilder body = new StringBuilder();
                    if (planId != null) {
                        body.append("Plan ").append(planId.substring(Math.max(0, planId.length() - 12))).append(" has been updated.");
                    } else {
                        body.append("A project you follow has been updated.");
                    }
                    content.setBody(body.toString());
                    
                    // Use scheduled time from context, or default to 1 hour from now
                    long scheduledTimeMs = context.scheduledTime != null ?
                        context.scheduledTime : (System.currentTimeMillis() + 3600000);
                    content.setScheduledTime(scheduledTimeMs);
                    
                    // Set notification properties
                    content.setPriority("default");
                    content.setSound(true);
                    
                    contents.add(content);
                }
            }
            
            // If no data items, create a default notification
            if (contents.isEmpty()) {
                NotificationContent defaultContent = new NotificationContent();
                defaultContent.setId("endorser_no_updates_" + System.currentTimeMillis());
                defaultContent.setTitle("No Project Updates");
                defaultContent.setBody("No updates found in your starred projects.");
                
                long scheduledTimeMs = context.scheduledTime != null ?
                    context.scheduledTime : (System.currentTimeMillis() + 3600000);
                defaultContent.setScheduledTime(scheduledTimeMs);
                defaultContent.setPriority("default");
                defaultContent.setSound(true);
                
                contents.add(defaultContent);
            }
            
        } catch (Exception e) {
            Log.e(TAG, "TestNativeFetcher: Error parsing API response", e);
            // Return empty list on parse error
        }
        
        return contents;
    }
}

