<?php
/**
 * Cast Conductor Proprietary License v5
 * SPDX-License-Identifier: LicenseRef-CastConductor-Proprietary-v5
 * 
 * Copyright (c) 2025 CastConductor.com. All Rights Reserved.
 * 
 * This file is part of Cast Conductor ("Software"). The Software and its source
 * code constitute proprietary, confidential, and trade secret information of
 * CastConductor.com ("Company"). Any access or use is governed strictly by the
 * Cast Conductor Proprietary License v5 ("License"). By installing, copying,
 * accessing, compiling, or otherwise using the Software you agree to be bound by
 * all terms of the License. If you do not agree, you must cease use immediately.
 * 
 * Key Terms (Summary – see full License for binding terms):
 *  1. No Redistribution: You may not publish, distribute, sublicense, rent,
 *     lease, transfer, sell, or otherwise make the Software (or any derivative)
 *     available to any third party without prior written consent of Company.
 *  2. No Modification: Modification, reverse engineering, decompilation, or
 *     disassembly is prohibited except to the limited extent expressly permitted
 *     by applicable law that cannot be contractually waived.
 *  3. Confidentiality: Treat all source code and related artifacts as Company
 *     Confidential Information. Maintain at least the same degree of care as for
 *     your own confidential materials, and not less than reasonable care.
 *  4. No Patent License: No express or implied patent rights are granted. Future
 *     patents (if any) are fully reserved.
 *  5. No Trademark License: Company names, marks, and logos may not be used
 *     without prior written permission.
 *  6. Limited Internal Use: Use is limited solely to internal evaluation and
 *     operation of licensed Cast Conductor deployments. Commercial hosting or
 *     resale as a service requires a separate written agreement.
 *  7. Telemetry & License Validation: The Software may periodically transmit a
 *     hashed installation identifier, domain (or site ID), plugin/app version,
 *     and a truncated (non-reversible) fragment of the license key solely to
 *     validate activation status and enforce licensing. This minimal "phone home"
 *     check contains no personal or content data. If optional telemetry is later
 *     introduced it will be limited to aggregate operational metrics (no PII),
 *     fully documented, and optionally disableable per published instructions.
 *  8. Third-Party Components: The Software may include open source components
 *     covered by their own licenses. See THIRD-PARTY-NOTICES.md. Those licenses
 *     govern their respective components; this License governs all remaining code.
 *  9. Export Compliance: You are responsible for compliance with all applicable
 *     export control and sanctions laws.
 * 10. Warranty Disclaimer: THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF
 *     ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO MERCHANTABILITY,
 *     FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT.
 * 11. Limitation of Liability: IN NO EVENT WILL COMPANY OR AUTHORS BE LIABLE FOR
 *     ANY INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE
 *     DAMAGES, OR LOST PROFITS, EVEN IF ADVISED OF THE POSSIBILITY.
 * 12. Acceptance: Use of the Software constitutes acceptance of the License.
 * 13. Enforcement: Unauthorized reproduction or distribution may result in civil
 *     and criminal penalties and will be prosecuted to the maximum extent allowed
 *     by law.
 * 
 * Authoritative EULA: EULA-v5.1.md (repository root – private) and https://castconductor.com/eula
 * Precedence: If this summary conflicts with the EULA, the EULA governs.
 * Revision: Current EULA revision v5.1 (subject to update; check EULA for current enterprise thresholds).
 * 
 * Full License text available from: licensing@castconductor.com
 * Security reports: security@castconductor.com
 * Commercial inquiries: licensing@castconductor.com
 * 
 * END OF HEADER
 */

/**
 * Roku API Controller
 * 
 * Handles the master app-config endpoint that provides complete configuration for Roku app
 * Serves content to Roku devices via REST API
 */

if (!defined('ABSPATH')) {
    exit;
}

class CastConductor_Roku_Controller extends WP_REST_Controller {
    
    /**
     * Get complete app configuration for Roku app
     * 
     * This is the MASTER ENDPOINT that Roku uses to get everything it needs
     */
    public static function get_app_config($request) {
        try {
            // Get Roku-specific parameters
            $roku_id = $request->get_param('roku_id');
            // Do NOT infer client IP automatically; only use when explicitly provided by Roku device
            $ip_address = $request->get_param('ip_address');

            // Viewer location is only fetched when Roku provides an IP; otherwise, defer to device runtime
            $viewer_location = is_string($ip_address) && $ip_address !== '' ? self::get_viewer_location($ip_address) : array();
            
            // Get scene using weighted selection (supports rotation percentages)
            // If no rotation is configured, falls back to simple active scene
            $db = new CastConductor_Database();
            $active_scene = $db->get_weighted_scene();
            $scene_overrides = array();
            if ($active_scene) {
                $scene_overrides = $db->get_scene_container_overrides((int)$active_scene->id);
            }

            // Get all active containers with their assigned content blocks (merged with scene overrides)
            // IMPORTANT: Pass the weighted scene to ensure containers match the selected scene
            $containers = self::get_active_containers_config($viewer_location, $scene_overrides, $active_scene);
            
            // Get global settings
            $global_settings = self::get_global_settings();
            
            // Get license status for watermark display
            $license_config = self::get_license_config();
            
            // Get advertising configuration (if enabled and available)
            $advertising_config = self::get_advertising_config();
            
            // Get streaming configuration (stream URL, metadata URL from WordPress settings)
            $streaming_config = self::get_streaming_config();
            
            // Build complete configuration
            $config = array(
                'containers' => $containers,
                'global_settings' => $global_settings,
                'license' => $license_config,
                'streaming' => $streaming_config,
                'advertising' => $advertising_config,
                'analytics' => self::get_analytics_config(),
                'scene' => $active_scene ? array(
                    'id' => (int) $active_scene->id,
                    'name' => $active_scene->name,
                    'branding' => self::safe_json_decode($active_scene->branding),
                    'background' => self::safe_json_decode($active_scene->background),
                    'metadata' => self::safe_json_decode($active_scene->metadata),
                ) : null,
                // Viewer context is intentionally minimal; Roku device derives its own location/time via GeoIP
                'viewer_context' => array(
                    'device_runtime' => true,
                    'ip_address' => is_string($ip_address) && $ip_address !== '' ? $ip_address : null,
                    'location' => !empty($viewer_location) ? $viewer_location : null,
                    'timezone' => !empty($viewer_location['timezone']) ? $viewer_location['timezone'] : null
                ),
                'api_endpoints' => self::get_api_endpoints(),
                'branding' => self::get_branding_config(),
                // Version handshake for Roku ↔ Plugin alignment
                'handshake' => array(
                    'api_major' => CastConductor_REST_API::API_MAJOR,
                    'api_namespace' => CastConductor_REST_API::API_NAMESPACE,
                    'plugin_version' => defined('CASTCONDUCTOR_VERSION') ? CASTCONDUCTOR_VERSION : get_bloginfo('version')
                ),
                // Debug logging configuration
                'debug' => array(
                    'enabled' => CastConductor_Debug::is_debug_enabled(),
                    'log_endpoint' => rest_url('castconductor/v5/debug/log'),
                    'log_interval_seconds' => 30, // How often Roku should POST buffered logs
                )
            );
            
            return CastConductor_REST_API::format_response($config);
            
        } catch (Exception $e) {
            error_log('CastConductor Roku API Error: ' . $e->getMessage());
            return CastConductor_REST_API::error_response(
                'Failed to load app configuration',
                'app_config_error',
                500
            );
        }
    }
    
    /**
     * Get stream configuration for Roku app
     */
    public static function get_stream_config($request) {
        // Get raw metadata URL from WordPress settings
        $raw_metadata_url = get_option('castconductor_metadata_url', '');
        
        // Automatically wrap with proxy to enable enhanced artwork discovery
        // This happens transparently - users just enter their metadata URL in Toaster
        // and the system automatically routes it through our enhancement layer
        $metadata_url = '';
        if (!empty($raw_metadata_url)) {
            $proxy_base = home_url('/wp-json/castconductor/v5/metadata/proxy');
            $metadata_url = add_query_arg(
                array(
                    'url' => $raw_metadata_url,
                    'format' => 'azuracast',
                    'enhance_artwork' => 'true'
                ),
                $proxy_base
            );
        }
        
        // Determine streaming mode
        $streaming_mode = get_option('castconductor_streaming_mode', 'auto');
        $video_url = get_option('castconductor_video_url', '');
        $audio_url = get_option('castconductor_stream_url', '');
        
        // Auto-detect mode if set to auto
        if ($streaming_mode === 'auto') {
            if (!empty($video_url)) {
                $streaming_mode = 'video';
            } elseif (!empty($audio_url)) {
                $streaming_mode = 'audio';
            } else {
                $streaming_mode = 'audio'; // Default fallback
            }
        }
        
        // Detect video format from URL
        $video_format = 'hls'; // Default
        if (!empty($video_url)) {
            $video_lower = strtolower($video_url);
            if (strpos($video_lower, '.m3u8') !== false) {
                $video_format = 'hls';
            } elseif (strpos($video_lower, 'rtmp://') === 0) {
                $video_format = 'rtmp';
            } elseif (strpos($video_lower, '.mp4') !== false) {
                $video_format = 'mp4';
            } elseif (strpos($video_lower, 'youtube.com') !== false || strpos($video_lower, 'youtu.be') !== false) {
                $video_format = 'youtube';
            } elseif (strpos($video_lower, 'twitch.tv') !== false) {
                $video_format = 'twitch';
            }
        }
        
        // Determine playback mode (Phase 4: Dual Playback Support)
        // Options: 'standard', 'dual_source', 'video_only', 'audio_only'
        $playback_mode = get_option('castconductor_playback_mode', 'standard');
        
        // Validate playback mode
        $valid_modes = array('standard', 'dual_source', 'video_only', 'audio_only');
        if (!in_array($playback_mode, $valid_modes)) {
            $playback_mode = 'standard';
        }
        
        // Auto-detect mode if set to 'standard' (backwards compatible)
        if ($playback_mode === 'standard') {
            if (!empty($video_url) && !empty($audio_url)) {
                // Both configured - keep as standard (video takes precedence)
                // User can explicitly set 'dual_source' if they want both
            } elseif (!empty($video_url)) {
                $playback_mode = 'video_only';
            } elseif (!empty($audio_url)) {
                $playback_mode = 'audio_only';
            }
        }
        
        $stream_config = array(
            'streaming_mode' => $streaming_mode, // Legacy: 'audio', 'video', or 'auto'
            'playback_mode' => $playback_mode,   // Phase 4: 'standard', 'dual_source', 'video_only', 'audio_only'
            'audio_stream' => array(
                'url' => $audio_url,
                'format' => 'mp3', // Default format
                'bitrate' => '320',
                'enabled' => !empty($audio_url)
            ),
            'video_stream' => array(
                'url' => $video_url,
                'format' => $video_format,
                'enabled' => !empty($video_url),
                'muted' => ($playback_mode === 'dual_source') // Mute video in dual source mode
            ),
            'metadata_api' => array(
                'url' => $metadata_url,
                'format' => 'azuracast', // Default format
                'refresh_interval' => 30,
                'enabled' => !empty($metadata_url)
            ),
            'fallback_config' => array(
                'show_offline_message' => true,
                'offline_message' => 'Stream temporarily unavailable',
                'retry_interval' => 60
            ),
            // Version handshake duplicated here for minimal clients
            'handshake' => array(
                'api_major' => CastConductor_REST_API::API_MAJOR,
                'api_namespace' => CastConductor_REST_API::API_NAMESPACE,
                'plugin_version' => defined('CASTCONDUCTOR_VERSION') ? CASTCONDUCTOR_VERSION : get_bloginfo('version')
            )
        );
        
        return CastConductor_REST_API::format_response($stream_config);
    }
    
    /**
     * Get a single content block for Roku overlay display
     * 
     * Public endpoint used when menu items open content blocks that aren't
     * part of the current scene (e.g., podcast feed overlays)
     * 
     * @param WP_REST_Request $request The request object
     * @return WP_REST_Response
     */
    public static function get_content_block_for_roku($request) {
        try {
            $content_block_id = $request->get_param('id');
            
            if (!$content_block_id) {
                return new WP_REST_Response(array(
                    'success' => false,
                    'message' => 'Content block ID is required'
                ), 400);
            }
            
            $database = new CastConductor_Database();
            $content_block = $database->get_content_block((int) $content_block_id);
            
            if (!$content_block) {
                return new WP_REST_Response(array(
                    'success' => false,
                    'message' => 'Content block not found'
                ), 404);
            }
            
            // Parse visual_config (canvas/layout data)
            $visual_config = array();
            if (!empty($content_block->visual_config)) {
                $visual_config = json_decode($content_block->visual_config, true);
                if (!is_array($visual_config)) {
                    $visual_config = array();
                }
            }
            
            // Parse data_config (data source settings)
            $data_config = array();
            if (!empty($content_block->data_config)) {
                $data_config = json_decode($content_block->data_config, true);
                if (!is_array($data_config)) {
                    $data_config = array();
                }
            }
            
            // Extract feed URL from layers (for feed-layer types)
            $feed_url = '';
            $feed_layer = null;
            if (!empty($visual_config['layers'])) {
                foreach ($visual_config['layers'] as $layer) {
                    if (isset($layer['kind']) && $layer['kind'] === 'feed-layer') {
                        $feed_layer = $layer;
                        $feed_url = $layer['feed_url'] ?? $layer['url'] ?? '';
                        break;
                    }
                }
            }
            
            // Parse the feed if we have a URL (uses WordPress transients for caching)
            $feed_items = array();
            if (!empty($feed_url)) {
                // Get max_items from feed layer config, default to 100
                $max_items = 100;
                if ($feed_layer !== null && !empty($feed_layer['max_items'])) {
                    $max_items = (int) $feed_layer['max_items'];
                }
                
                error_log("CastConductor: Fetching feed for overlay: {$feed_url} (max_items: {$max_items})");
                if (class_exists('CastConductor_Feed_Parser')) {
                    // Feed parser uses transients (database on shared hosting, Redis when available)
                    // Default cache: 5 minutes, auto-detects feed type
                    $parser = new CastConductor_Feed_Parser();
                    $feed_result = $parser->parse($feed_url, 'auto', array(
                        'cache_duration' => 300, // 5 minutes
                        'max_items' => $max_items
                    ));
                    
                    if (!is_wp_error($feed_result) && !empty($feed_result['items'])) {
                        $feed_items = $feed_result['items'];
                        error_log("CastConductor: Parsed " . count($feed_items) . " feed items (cached via transients)");
                    } elseif (is_wp_error($feed_result)) {
                        error_log("CastConductor: Feed parse error: " . $feed_result->get_error_message());
                    } else {
                        error_log("CastConductor: Feed parse returned empty items");
                    }
                } else {
                    error_log("CastConductor: Feed parser class not found");
                }
            }
            
            // Get layout dimensions from visual config
            $layout = $visual_config['layout'] ?? array();
            
            // Build content block response for Roku
            $block_data = array(
                'id' => (int) $content_block->id,
                'name' => $content_block->name,
                'type' => $content_block->type,
                'config' => $data_config,
                'visual_config' => $visual_config,
                'feed_url' => $feed_url,
                'feed_layer' => $feed_layer,
                'feed_items' => $feed_items,
                'width' => (int) ($layout['width'] ?? 1280),
                'height' => (int) ($layout['height'] ?? 720),
            );
            
            return CastConductor_REST_API::format_response($block_data);
            
        } catch (Exception $e) {
            error_log('CastConductor: Error getting content block for Roku: ' . $e->getMessage());
            return new WP_REST_Response(array(
                'success' => false,
                'message' => 'Internal server error'
            ), 500);
        }
    }
    
    /**
     * Get active containers with their content blocks
     * 
     * @param array $viewer_location Viewer location data
     * @param array $scene_overrides Scene-specific container overrides
     * @param object|null $scene The scene to get containers for (from weighted selection)
     */
    private static function get_active_containers_config($viewer_location, $scene_overrides = array(), $scene = null) {
        global $wpdb;
        
        $database = new CastConductor_Database();
        
        // Use passed scene (from weighted selection) or fall back to active scene
        $active_scene = $scene ?? $database->get_active_scene();
        if (!$active_scene) {
            error_log('CastConductor V5: No active scene found, returning empty containers array');
            return array();
        }
        
        error_log(sprintf('CastConductor V5: Active scene id=%d, name=%s', $active_scene->id, $active_scene->name));
        
        // Get containers assigned to the active scene
        $containers_table = $database->get_table_name('containers');
        $scene_containers_table = $database->get_table_name('scene_containers');
        
        // First check if this scene has entries in scene_containers table
        $scene_assignment_count = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$scene_containers_table} WHERE scene_id = %d",
            $active_scene->id
        ));
        error_log(sprintf('CastConductor V5: Scene %d has %d container assignments in scene_containers', $active_scene->id, $scene_assignment_count));
        
        if ($scene_assignment_count > 0) {
            // Scene has explicit container assignments - use them
            $query = $wpdb->prepare(
                "SELECT c.* FROM {$containers_table} c
                INNER JOIN {$scene_containers_table} sc ON c.id = sc.container_id
                WHERE c.enabled = 1 
                AND sc.scene_id = %d 
                AND sc.enabled = 1
                ORDER BY c.id ASC",
                $active_scene->id
            );
        } else {
            // FALLBACK: Scene has no scene_containers entries (legacy/default scene)
            // Load all enabled containers from the containers table directly
            // This supports scenes created before scene_containers system was implemented
            error_log(sprintf('CastConductor V5: Scene %d has no scene_containers entries, using FALLBACK to load all enabled containers', $active_scene->id));
            $query = "SELECT c.* FROM {$containers_table} c WHERE c.enabled = 1 ORDER BY c.id ASC";
        }
        
        error_log(sprintf('CastConductor V5: Query: %s', $query));
        
        $containers = $wpdb->get_results($query);
        
        error_log(sprintf('CastConductor V5: Found %d containers for scene %d', count($containers), $active_scene->id));
        
        if ($containers) {
            foreach ($containers as $idx => $c) {
                error_log(sprintf('CastConductor V5: Container[%d]: id=%d name=%s position=%s enabled=%d', $idx, $c->id, $c->name, $c->position, $c->enabled));
            }
        }
        
        $containers_config = array();
        
        foreach ($containers as $container) {
            $container_data = array(
                'id' => (int) $container->id,
                'name' => $container->name,
                'position' => $container->position,
                'width' => (int) $container->width,
                'height' => (int) $container->height,
                'x_position' => (int) $container->x_position,
                'y_position' => (int) $container->y_position,
                'z_index' => (int) $container->z_index,
                'rotation_enabled' => (bool) $container->rotation_enabled,
                'rotation_interval' => (int) $container->rotation_interval,
                'content_blocks' => array()
            );

            // V5 GEOMETRY: Native 1280x720 pass-through. No scaling.

            // Apply scene overrides for geometry if present
            $cid = (int) $container->id;
            if (isset($scene_overrides[$cid]) && isset($scene_overrides[$cid]['overrides']) && is_array($scene_overrides[$cid]['overrides'])) {
                $ov = $scene_overrides[$cid]['overrides'];
                foreach (['width','height','x_position','y_position','z_index','rotation_enabled','rotation_interval'] as $k) {
                    if (isset($ov[$k])) {
                        $container_data[$k] = $ov[$k];
                    }
                }
            }

            // Include Phase 3 layout metadata for Roku parity
            // Priority: scene_overrides.overrides.layout > WordPress options > default
            $layout = self::get_container_layout((int)$container->id);
            
            // Check if scene has layout overrides (zones defined per-scene)
            if (isset($scene_overrides[$cid]) && 
                isset($scene_overrides[$cid]['overrides']) && 
                isset($scene_overrides[$cid]['overrides']['layout']) &&
                is_array($scene_overrides[$cid]['overrides']['layout'])) {
                $scene_layout = $scene_overrides[$cid]['overrides']['layout'];
                // Merge scene layout with defaults, scene takes priority
                if (isset($scene_layout['zones']) && is_array($scene_layout['zones'])) {
                    $layout['zones'] = $scene_layout['zones'];
                }
                if (isset($scene_layout['activeZoneIds'])) {
                    $layout['activeZoneIds'] = $scene_layout['activeZoneIds'];
                }
                error_log(sprintf('CastConductor V5: Using scene layout override for container %d with %d zones', $cid, count($layout['zones'])));
            }
            
            // Ensure layout zones reside in Roku space
            if (is_array($layout)) {
                // V5 GEOMETRY: No scaling.
                $authored_space = isset($layout['authored_space']) ? $layout['authored_space'] : 'roku_1280x720';
                
                // Pass through zones as-is
                $layout['authored_space'] = $authored_space;
            }
            $container_data['layout'] = $layout;
            
            // Include per-zone assignments if any (Phase 3 parity)
            $zone_assignments = self::get_zone_assignments_map((int)$container->id);
            // If scene specifies per-zone assignments, overlay them (opt-in per-zone)
            if (isset($scene_overrides[$cid]) && isset($scene_overrides[$cid]['zones']) && is_array($scene_overrides[$cid]['zones'])) {
                foreach ($scene_overrides[$cid]['zones'] as $zoneId => $zoneData) {
                    // Scene-container zones format: {zone_id: {assignments: [{block_id, weight, interval, order}]}}
                    // Extract the assignments array if wrapped
                    if (is_array($zoneData) && isset($zoneData['assignments'])) {
                        $zone_assignments[$zoneId] = $zoneData['assignments'];
                    } else {
                        $zone_assignments[$zoneId] = $zoneData;
                    }
                }
            }
            if (!empty($zone_assignments)) {
                error_log(sprintf('CastConductor V5: Processing zone assignments for container %d: %s', $cid, json_encode($zone_assignments)));
                $enriched = array();
                foreach ($zone_assignments as $zone_id => $list) {
                    if (!is_array($list)) {
                        error_log(sprintf('CastConductor V5: Skipping zone %s - not an array', $zone_id));
                        continue;
                    }
                    error_log(sprintf('CastConductor V5: Processing zone %s with %d assignments', $zone_id, count($list)));
                    $zone_blocks = array();
                    foreach ($list as $row) {
                        // Respect enabled flag on assignment
                        $enabled = isset($row['enabled']) ? (int)$row['enabled'] : 1;
                        if ($enabled !== 1) {
                            error_log(sprintf('CastConductor V5: Skipping assignment - disabled (enabled=%d)', $enabled));
                            continue;
                        }
                        $block_id = isset($row['block_id']) ? (int)$row['block_id'] : 0;
                        if ($block_id <= 0) {
                            error_log('CastConductor V5: Skipping assignment - invalid block_id');
                            continue;
                        }
                        error_log(sprintf('CastConductor V5: Looking up content block %d', $block_id));

                        // Load content block details
                        $blocks_table = $database->get_table_name('content_blocks');
                        $block = $wpdb->get_row($wpdb->prepare(
                            "SELECT id, name, type, visual_config, data_config, enabled FROM {$blocks_table} WHERE id = %d",
                            $block_id
                        ));
                        if (!$block) {
                            error_log(sprintf('CastConductor V5: Content block %d not found in database', $block_id));
                            continue;
                        }
                        if ((int)$block->enabled !== 1) {
                            error_log(sprintf('CastConductor V5: Content block %d (%s) is disabled', $block_id, $block->name));
                            continue;
                        }
                        error_log(sprintf('CastConductor V5: Loaded content block %d (%s, type=%s)', $block_id, $block->name, $block->type));

                        // Build assignment-like object to reuse data source logic
                        // Map scene-container zone format (weight/interval/order) to rotation format
                        $rotation_order = isset($row['order']) ? (int)$row['order'] : (isset($row['rotation_order']) ? (int)$row['rotation_order'] : 0);
                        $rotation_percentage = isset($row['weight']) ? (float)$row['weight'] : (isset($row['rotation_percentage']) ? (float)$row['rotation_percentage'] : 0.0);
                        
                        $assignment_like = (object) array(
                            'content_block_id' => (int)$block->id,
                            'name' => $block->name,
                            'type' => $block->type,
                            'visual_config' => $block->visual_config,
                            'data_config' => $block->data_config,
                            'rotation_order' => $rotation_order,
                            'rotation_percentage' => $rotation_percentage
                        );

                        $content_block = self::get_content_block_data($assignment_like, $viewer_location);
                        if ($content_block) {
                            $zone_blocks[] = $content_block;
                        }
                    }
                    // Sort zone blocks by rotation_order for deterministic rotation
                    usort($zone_blocks, function ($a, $b) {
                        return ($a['rotation_order'] <=> $b['rotation_order']);
                    });
                    // Only include zone if it has actual content blocks (skip empty zones)
                    if (!empty($zone_blocks)) {
                        error_log(sprintf('CastConductor V5: Zone %s has %d content blocks', $zone_id, count($zone_blocks)));
                        $enriched[$zone_id] = $zone_blocks;
                    } else {
                        error_log(sprintf('CastConductor V5: Zone %s has no content blocks after processing', $zone_id));
                    }
                }
                if (!empty($enriched)) {
                    error_log(sprintf('CastConductor V5: Container %d has %d enriched zones', $cid, count($enriched)));
                    $container_data['zone_assignments'] = $enriched;
                } else {
                    error_log(sprintf('CastConductor V5: Container %d has no enriched zones', $cid));
                }
            } else {
                error_log(sprintf('CastConductor V5: Container %d has no zone assignments', $cid));
            }

            // Include overlay configuration for menu/navigation system (Phase 1)
            $overlay_config = self::get_container_overlay((int)$container->id);
            if ($overlay_config && isset($overlay_config['display_mode'])) {
                $container_data['overlay'] = $overlay_config;
            }
            
            // Get assigned content blocks for this container
            $assignments = $database->get_container_assignments($container->id);
            
            foreach ($assignments as $assignment) {
                $content_block = self::get_content_block_data($assignment, $viewer_location);
                if ($content_block) {
                    $container_data['content_blocks'][] = $content_block;
                }
            }
            
            // Only include containers that have content blocks or zone assignments
            if (!empty($container_data['content_blocks']) || !empty($container_data['zone_assignments'])) {
                error_log(sprintf('CastConductor V5: Including container %d in response (content_blocks=%d, zone_assignments=%d)',
                    $cid, count($container_data['content_blocks']), isset($container_data['zone_assignments']) ? count($container_data['zone_assignments']) : 0));
                $containers_config[] = $container_data;
            } else {
                error_log(sprintf('CastConductor V5: EXCLUDING container %d - no content blocks or zone assignments', $cid));
            }
        }
        
        error_log(sprintf('CastConductor V5: Returning %d containers in final response', count($containers_config)));
        
        return $containers_config;
    }

    private static function safe_json_decode($value) {
        if (is_array($value) || is_object($value)) { return json_decode(wp_json_encode($value), true); }
        if (is_string($value) && $value !== '') {
            $d = json_decode($value, true);
            return (json_last_error() === JSON_ERROR_NONE && is_array($d)) ? $d : array();
        }
        return array();
    }

    /**
     * Retrieve container layout (zones) – mirrors Containers Controller storage
     */
    private static function get_container_layout($container_id) {
        $key = 'castconductor_container_layout_' . intval($container_id);
        $raw = get_option($key, null);
        if (is_string($raw)) {
            $decoded = json_decode($raw, true);
            if (is_array($decoded)) return $decoded;
        } elseif (is_array($raw)) {
            return $raw;
        }
        return array('zones' => array(), 'activeZoneIds' => array());
    }

    /**
     * Retrieve per-zone assignments map for a container
     */
    private static function get_zone_assignments_map($container_id) {
        $key = 'castconductor_container_zone_assignments_' . intval($container_id);
        $raw = get_option($key, null);
        if (is_string($raw)) {
            $decoded = json_decode($raw, true);
            if (is_array($decoded)) return $decoded;
        } elseif (is_array($raw)) {
            return $raw;
        }
        return array();
    }

    /**
     * Retrieve per-container overlay configuration for menu/navigation system
     * Phase 1: Menu & Navigation System (SPEC-MENU-NAVIGATION.md)
     */
    private static function get_container_overlay($container_id) {
        $key = 'castconductor_container_overlay_' . intval($container_id);
        $raw = get_option($key, null);
        $default = array(
            'display_mode' => 'visible',
            'trigger' => 'none',
            'position' => 'bottom',
            'animation' => 'slide',
            'dismiss_on_select' => true,
            'background_color' => '#000000CC',
            'auto_dismiss_seconds' => 0
        );
        if ($raw === null) return $default;
        if (is_string($raw)) {
            $decoded = json_decode($raw, true);
            if (is_array($decoded)) return array_merge($default, $decoded);
        }
        if (is_array($raw)) return array_merge($default, $raw);
        return $default;
    }
    
    /**
     * Get content block data with real data sources
     */
    private static function get_content_block_data($assignment, $viewer_location) {
        $visual_config = json_decode($assignment->visual_config, true);
        $data_config = json_decode($assignment->data_config, true);
        
        $content_block = array(
            'id' => $assignment->content_block_id,
            'name' => $assignment->name,
            'type' => $assignment->type,
            'visual_config' => $visual_config,
            'rotation_order' => (int)$assignment->rotation_order,
            'rotation_percentage' => (float)$assignment->rotation_percentage
        );
        
        // Add real data source configuration based on block type
        switch ($assignment->type) {
            case 'track_info':
                $content_block['data_source'] = self::get_track_info_data_source($data_config);
                break;
                
            case 'weather':
                $content_block['data_source'] = self::get_weather_data_source($data_config, $viewer_location);
                break;
                
            case 'location_time':
                $content_block['data_source'] = self::get_location_time_data_source($data_config, $viewer_location);
                break;
                
            case 'shoutout':
                $content_block['data_source'] = self::get_shoutout_data_source($data_config);
                break;
                
            case 'sponsor':
                $content_block['data_source'] = self::get_sponsor_data_source($data_config);
                break;
                
            case 'promo':
                $content_block['data_source'] = self::get_promo_data_source($data_config);
                break;
                
            case 'custom_api':
                $content_block['data_source'] = self::get_custom_api_data_source($data_config);
                break;
                
            case 'custom':
                $content_block['data_source'] = self::get_custom_data_source($data_config);
                break;
        }
        
        return $content_block;
    }
    
    /**
     * Get track info data source (REAL METADATA)
     */
    private static function get_track_info_data_source($data_config) {
        $raw_metadata_url = $data_config['endpoint_override'] ?? get_option('castconductor_metadata_url', '');
        
        // ALWAYS wrap metadata URL with proxy for artwork enhancement
        // The proxy fetches metadata and enhances artwork using our discovery APIs
        $proxy_url = '';
        if (!empty($raw_metadata_url)) {
            $proxy_url = add_query_arg(array(
                'url' => $raw_metadata_url,
                'format' => 'azuracast',
                'enhance_artwork' => 'true'
            ), home_url('/wp-json/castconductor/v5/metadata/proxy'));
        }
        
        return array(
            'type' => 'audio_metadata',
            'endpoint' => $proxy_url,
            'format' => 'azuracast', // Default format
            'refresh_interval' => $data_config['refresh_interval'] ?? 15,
            'album_artwork' => array(
                'enabled' => get_option('castconductor_artwork_search_enabled', true),
                'search_endpoint' => home_url('/wp-json/castconductor/v5/artwork/search'),
                'fallback_image' => get_option('castconductor_current_square_logo_600x600', '')
            ),
            'fallback_data' => array(
                'artist' => 'Now Playing',
                'title' => 'Live Stream',
                'album' => ''
            )
        );
    }
    
    /**
     * Get weather data source (REAL IP GEOLOCATION)
     */
    private static function get_weather_data_source($data_config, $viewer_location) {
        $api_key = get_option('castconductor_openweather_api_key', '');
        // If no viewer location is provided, delegate weather to Roku device runtime
        if (empty($viewer_location) || empty($viewer_location['city'])) {
            return array(
                'type' => 'device_runtime',
                'provider' => 'openweathermap',
                'refresh_interval' => $data_config['refresh_interval'] ?? 600,
                'enabled' => true
            );
        }
        return array(
            'type' => 'openweathermap_api',
            'api_key' => $api_key,
            'location' => array(
                'city' => $viewer_location['city'] ?? '',
                'latitude' => $viewer_location['latitude'] ?? '',
                'longitude' => $viewer_location['longitude'] ?? ''
            ),
            'refresh_interval' => $data_config['refresh_interval'] ?? 600,
            'enabled' => !empty($api_key) && !empty($viewer_location['city'])
        );
    }
    
    /**
     * Get location/time data source (REAL ROKU VIEWER GEOLOCATION)
     */
    private static function get_location_time_data_source($data_config, $viewer_location) {
        // If no server-side location, instruct Roku device to resolve location/time locally
        if (empty($viewer_location)) {
            return array(
                'type' => 'device_runtime',
                'refresh_interval' => $data_config['refresh_interval'] ?? 60,
                'enabled' => true
            );
        }
        return array(
            'type' => 'viewer_location',
            'location' => $viewer_location,
            'timezone' => $viewer_location['timezone'] ?? wp_timezone_string(),
            'refresh_interval' => $data_config['refresh_interval'] ?? 60,
            'enabled' => !empty($viewer_location['city'])
        );
    }
    
    /**
     * Get shoutout data source (REAL ADMIN TEST DATA)
     */
    private static function get_shoutout_data_source($data_config) {
        return array(
            'type' => 'wordpress_posts',
            'post_type' => 'cc_shoutout',
            'endpoint' => home_url('/wp-json/castconductor/v5/content/shoutouts/active'),
            'refresh_interval' => 300, // 5 minutes
            'limit' => $data_config['limit'] ?? 10
        );
    }
    
    /**
     * Get sponsor data source (REAL ADMIN TEST CAMPAIGNS)
     */
    private static function get_sponsor_data_source($data_config) {
        return array(
            'type' => 'wordpress_posts',
            'post_type' => 'cc_sponsor',
            'endpoint' => home_url('/wp-json/castconductor/v5/content/sponsors/active'),
            'refresh_interval' => 600, // 10 minutes
            'scheduling_enabled' => true
        );
    }
    
    /**
     * Get promo data source (REAL ADMIN TEST CONTENT)
     */
    private static function get_promo_data_source($data_config) {
        return array(
            'type' => 'wordpress_posts',
            'post_type' => 'cc_promo',
            'endpoint' => home_url('/wp-json/castconductor/v5/content/promos/active'),
            'refresh_interval' => 600, // 10 minutes
            'scheduling_enabled' => true
        );
    }
    
    /**
     * Get custom API data source
     * Enhanced for Phase 1 MVP (Dec 2025) with field mapping support
     * 
     * For authenticated APIs: Pre-fetches data server-side, caches it,
     * and sends resolved_data to Roku (keys never leave server)
     * 
     * For no-auth APIs: Sends URL directly for Roku to fetch
     */
    private static function get_custom_api_data_source($data_config) {
        // Extract API configuration
        $api_config = $data_config['api_config'] ?? [];
        $api_url = $api_config['api_url'] ?? ($data_config['api_url'] ?? '');
        $refresh_interval = $api_config['refresh_interval'] ?? ($data_config['refresh_interval'] ?? 300);
        $http_method = $api_config['http_method'] ?? 'GET';
        $timeout = $api_config['timeout'] ?? 10;
        $cache_duration = $api_config['cache_duration'] ?? 1800;
        
        // Extract auth configuration
        $auth_config = $api_config['auth'] ?? ['type' => 'none'];
        $auth_type = $auth_config['type'] ?? 'none';
        $requires_auth = ($auth_type !== 'none');
        
        // Extract data path configuration (for arrays)
        $data_path = $data_config['data_path'] ?? [];
        $array_path = $data_path['array_path'] ?? '';
        $array_selection = $data_path['array_selection'] ?? 'first';
        
        // Extract field mappings (JSONPath)
        $field_mapping = $data_config['field_mapping'] ?? [];
        
        // Extract display options
        $display_options = $data_config['display_options'] ?? [];
        
        // Base data source
        $data_source = array(
            'type' => 'external_api',
            'http_method' => $http_method,
            'timeout' => $timeout,
            'cache_duration' => $cache_duration,
            'refresh_interval' => $refresh_interval,
            'enabled' => !empty($api_url),
            'requires_auth' => $requires_auth,
            // Data path for arrays
            'array_path' => $array_path,
            'array_selection' => $array_selection,
            // Field mappings (JSONPath → display fields)
            'field_mapping' => array(
                'primary_text' => $field_mapping['primary_text'] ?? '',
                'secondary_text' => $field_mapping['secondary_text'] ?? '',
                'image_url' => $field_mapping['image_url'] ?? '',
                'number_value' => $field_mapping['number_value'] ?? '',
                'date_value' => $field_mapping['date_value'] ?? '',
                'link_url' => $field_mapping['link_url'] ?? ''
            ),
            // Display options
            'display_options' => array(
                'show_primary' => $display_options['show_primary'] ?? true,
                'show_secondary' => $display_options['show_secondary'] ?? true,
                'show_image' => $display_options['show_image'] ?? true,
                'show_number' => $display_options['show_number'] ?? false,
                'number_prefix' => $display_options['number_prefix'] ?? '',
                'number_suffix' => $display_options['number_suffix'] ?? '',
                'text_truncate' => $display_options['text_truncate'] ?? 100
            )
        );
        
        if ($requires_auth) {
            // Authenticated API: Pre-fetch and cache server-side
            // API keys NEVER sent to Roku device
            $resolved = self::prefetch_authenticated_api($api_url, $auth_config, $cache_duration, $timeout, $http_method);
            $data_source['resolved_data'] = $resolved['data'];
            $data_source['resolved_at'] = $resolved['timestamp'];
            $data_source['cache_ttl'] = $cache_duration;
            // Don't send api_url to Roku for authenticated APIs (security)
        } else {
            // No auth required: Use WordPress proxy by default
            // This provides caching, rate limit protection, and field mapping server-side
            $proxy_params = [
                'url' => $api_url,
                'cache' => $cache_duration,
            ];
            
            // Add path extraction if configured
            if (!empty($array_path)) {
                $proxy_params['path'] = $array_path;
            }
            
            // Add field mappings to proxy URL
            $fm = $data_source['field_mapping'];
            if (!empty($fm['primary_text'])) {
                $proxy_params['primary'] = $fm['primary_text'];
            }
            if (!empty($fm['secondary_text'])) {
                $proxy_params['secondary'] = $fm['secondary_text'];
            }
            if (!empty($fm['image_url'])) {
                $proxy_params['image'] = $fm['image_url'];
            }
            if (!empty($fm['date_value'])) {
                $proxy_params['date'] = $fm['date_value'];
            }
            
            // Generate proxy URL - Roku will fetch this instead of direct API
            $proxy_url = rest_url('castconductor/v5/external-api/proxy') . '?' . http_build_query($proxy_params);
            
            $data_source['api_url'] = $proxy_url;
            $data_source['direct_api_url'] = $api_url; // Keep original for reference/debugging
            $data_source['use_proxy'] = true;
        }
        
        return $data_source;
    }
    
    /**
     * Pre-fetch authenticated API data server-side
     * Caches response in WordPress transients
     * 
     * @param string $api_url The API endpoint URL
     * @param array $auth_config Authentication configuration
     * @param int $cache_duration Cache TTL in seconds
     * @param int $timeout Request timeout in seconds
     * @param string $http_method HTTP method (GET/POST)
     * @return array ['data' => mixed, 'timestamp' => string]
     */
    private static function prefetch_authenticated_api($api_url, $auth_config, $cache_duration = 300, $timeout = 10, $http_method = 'GET') {
        // Generate cache key based on URL (auth config intentionally not included for security)
        $cache_key = 'cc_api_' . md5($api_url);
        
        // Check cache first
        $cached = get_transient($cache_key);
        if ($cached !== false) {
            return $cached;
        }
        
        // Build request headers based on auth type
        $headers = [
            'Accept' => 'application/json',
            'User-Agent' => 'CastConductor/5.0 (WordPress Plugin)'
        ];
        
        $auth_type = $auth_config['type'] ?? 'none';
        $auth_key_name = $auth_config['key_name'] ?? '';
        $auth_key_value = $auth_config['key_value'] ?? '';
        $auth_secret = $auth_config['secret'] ?? '';
        
        $request_url = $api_url;
        
        switch ($auth_type) {
            case 'header':
                // API key in custom header
                if (!empty($auth_key_name) && !empty($auth_key_value)) {
                    $headers[$auth_key_name] = $auth_key_value;
                }
                break;
                
            case 'query':
                // API key as query parameter
                if (!empty($auth_key_name) && !empty($auth_key_value)) {
                    $separator = (strpos($request_url, '?') !== false) ? '&' : '?';
                    $request_url .= $separator . urlencode($auth_key_name) . '=' . urlencode($auth_key_value);
                }
                break;
                
            case 'bearer':
                // Bearer token
                if (!empty($auth_key_value)) {
                    $headers['Authorization'] = 'Bearer ' . $auth_key_value;
                }
                break;
                
            case 'basic':
                // Basic auth
                if (!empty($auth_key_value)) {
                    $credentials = $auth_key_value;
                    if (!empty($auth_secret)) {
                        $credentials .= ':' . $auth_secret;
                    }
                    $headers['Authorization'] = 'Basic ' . base64_encode($credentials);
                }
                break;
        }
        
        // Make the request
        $args = [
            'method' => $http_method,
            'timeout' => $timeout,
            'headers' => $headers
        ];
        
        $response = wp_remote_request($request_url, $args);
        
        $result = [
            'data' => null,
            'timestamp' => current_time('c'),
            'error' => null
        ];
        
        if (is_wp_error($response)) {
            error_log('CastConductor: Pre-fetch API error: ' . $response->get_error_message());
            $result['error'] = $response->get_error_message();
        } else {
            $status_code = wp_remote_retrieve_response_code($response);
            $body = wp_remote_retrieve_body($response);
            
            if ($status_code >= 200 && $status_code < 300) {
                $data = json_decode($body, true);
                if (json_last_error() === JSON_ERROR_NONE) {
                    $result['data'] = $data;
                } else {
                    $result['error'] = 'Invalid JSON response';
                    error_log('CastConductor: Pre-fetch API returned invalid JSON');
                }
            } else {
                $result['error'] = "HTTP {$status_code}";
                error_log("CastConductor: Pre-fetch API returned HTTP {$status_code}");
            }
        }
        
        // Cache the result (even errors, to prevent hammering)
        set_transient($cache_key, $result, $cache_duration);
        
        return $result;
    }
    
    /**
     * Get custom data source
     */
    private static function get_custom_data_source($data_config) {
        return array(
            'type' => 'static_content',
            'content' => $data_config['content'] ?? '',
            'refresh_interval' => 0,
            'enabled' => !empty($data_config['content'])
        );
    }
    
    /**
     * Get global settings
     */
    private static function get_global_settings() {
        return array(
            'station_name' => get_bloginfo('name'),
            'timezone' => wp_timezone_string(),
            'version' => CASTCONDUCTOR_VERSION,
            'debug_mode' => defined('WP_DEBUG') && WP_DEBUG,
            'refresh_intervals' => array(
                'metadata' => 30,
                'weather' => 600,
                'location' => 60,
                'content' => 300,
                'app_config' => (int) get_option('castconductor_scene_rotation_interval', 60)  // Scene rotation check interval (seconds)
            )
        );
    }
    
    /**
     * Get streaming configuration for Roku app
     * Returns stream URL and metadata URL from WordPress settings
     */
    private static function get_streaming_config() {
        $stream_url = get_option('castconductor_stream_url', '');
        $metadata_url = get_option('castconductor_metadata_url', '');
        
        // Build proxy-wrapped metadata URL if configured
        $proxy_base = rest_url('castconductor/v5/metadata/proxy');
        if (!empty($metadata_url)) {
            $metadata_url = add_query_arg(
                array(
                    'url' => $metadata_url,
                    'format' => 'azuracast',
                    'enhance_artwork' => 'true'
                ),
                $proxy_base
            );
        }
        
        return array(
            'stream_url' => $stream_url,
            'metadata_url' => $metadata_url,
            'video_url' => get_option('castconductor_video_url', ''),
            'format' => 'azuracast',
            'refresh_interval' => 15
        );
    }
    
    /**
     * Get API endpoints for Roku app
     */
    private static function get_api_endpoints() {
        $base_url = home_url('/wp-json/castconductor/v5');
        
        return array(
            'base_url' => $base_url,
            'artwork_search' => $base_url . '/artwork/search',
            'shoutouts_active' => $base_url . '/content/shoutouts/active',
            'sponsors_active' => $base_url . '/content/sponsors/active',
            'promos_active' => $base_url . '/content/promos/active'
        );
    }
    
    /**
     * Get branding configuration
     */
    private static function get_branding_config() {
        $branding_config = get_option('castconductor_branding_config', array());
        
        return array(
            'square_logo' => array(
                'enabled' => $branding_config['square_logo_enabled'] ?? true,
                'url' => wp_get_attachment_url(get_option('castconductor_current_square_logo_600x600')),
                'fallback_mode' => $branding_config['square_logo_fallback_mode'] ?? 'when_no_album_artwork',
                'position' => $branding_config['square_logo_position'] ?? array('x' => 60, 'y' => 740),
                'scale' => $branding_config['square_logo_scale'] ?? 1.0
            ),
            'animated_center_logo' => array(
                'enabled' => $branding_config['animated_center_logo_enabled'] ?? true,
                'url' => wp_get_attachment_url(get_option('castconductor_current_animated_center_logo_1280x250')),
                'duration' => $branding_config['animated_center_logo_duration'] ?? 5,
                'position' => $branding_config['animated_center_logo_position'] ?? array('x' => 640, 'y' => 415),
                'scale' => $branding_config['animated_center_logo_scale'] ?? 1.0
            ),
            'background' => array(
                'enabled' => $branding_config['background_enabled'] ?? true,
                'url' => wp_get_attachment_url(get_option('castconductor_current_background_1920x1080')),
                'opacity' => $branding_config['background_opacity'] ?? 0.3,
                'scale' => $branding_config['background_scale'] ?? 1.0
            ),
            'attribution' => $branding_config['branding_attribution'] ?? 'Powered by CastConductor'
        );
    }
    
    /**
     * Get viewer location from IP address
     */
    private static function get_viewer_location($ip_address) {
        // Check cache first
        $cache_key = 'castconductor_location_' . md5($ip_address);
        $cached_location = get_transient($cache_key);
        
        if ($cached_location !== false) {
            return $cached_location;
        }
        
        // Get location via IP geolocation service
        $location_data = self::get_ip_geolocation($ip_address);
        
        // Cache for 1 hour
        if ($location_data) {
            set_transient($cache_key, $location_data, HOUR_IN_SECONDS);
        }
        
        return $location_data ?: array();
    }
    
    /**
     * Get IP geolocation data
     */
    private static function get_ip_geolocation($ip_address) {
        // Skip for local development
        if (in_array($ip_address, array('127.0.0.1', '::1', 'localhost'))) {
            return array(
                'city' => 'Local Development',
                'state' => 'Local',
                'country' => 'Local',
                'latitude' => 0,
                'longitude' => 0,
                'timezone' => wp_timezone_string()
            );
        }
        
        // Use ipinfo.io (free, no API key required)
        $api_url = "https://ipinfo.io/{$ip_address}/json";
        
        $response = wp_remote_get($api_url, array(
            'timeout' => 5,
            'headers' => array(
                'User-Agent' => 'CastConductor/5.0.0'
            )
        ));
        
        if (is_wp_error($response)) {
            error_log('CastConductor: IP geolocation failed: ' . $response->get_error_message());
            return null;
        }
        
        $data = json_decode(wp_remote_retrieve_body($response), true);
        
        if (empty($data) || isset($data['error'])) {
            return null;
        }
        
        // Parse location data
        $location_parts = explode(',', $data['loc'] ?? '0,0');
        $city_region = explode(',', $data['region'] ?? '');
        
        return array(
            'city' => $data['city'] ?? '',
            'state' => $data['region'] ?? '',
            'country' => $data['country'] ?? '',
            'latitude' => (float)($location_parts[0] ?? 0),
            'longitude' => (float)($location_parts[1] ?? 0),
            'timezone' => $data['timezone'] ?? wp_timezone_string(),
            'postal_code' => $data['postal'] ?? '',
            'org' => $data['org'] ?? ''
        );
    }
    
    /**
     * Get client IP address
     */
    private static function get_client_ip() {
        $ip_keys = array(
            'HTTP_CF_CONNECTING_IP', // Cloudflare
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_FORWARDED',
            'HTTP_X_CLUSTER_CLIENT_IP',
            'HTTP_FORWARDED_FOR',
            'HTTP_FORWARDED',
            'REMOTE_ADDR'
        );
        
        foreach ($ip_keys as $key) {
            if (array_key_exists($key, $_SERVER) === true) {
                $ip = $_SERVER[$key];
                if (strpos($ip, ',') !== false) {
                    $ip = explode(',', $ip)[0];
                }
                $ip = trim($ip);
                
                if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                    return $ip;
                }
            }
        }
        
        return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
    }
    
    /**
     * Get advertising configuration for Roku app
     * 
     * Returns advertising settings if the feature is available and enabled.
     * Feature availability is gated by license plan:
     * - Business/Enterprise: Included
     * - Creator/Pro: Available as $29/mo add-on
     * 
     * @return array|null Advertising configuration or null if not enabled
     */
    private static function get_advertising_config() {
        // Get advertising manager instance
        $advertising = CastConductor_Advertising::instance();
        
        // Return advertising config (null if not enabled/available)
        return $advertising->get_advertising_config();
    }

    /**
     * Get analytics configuration for Roku app
     * 
     * Returns the API key and endpoint configuration for sending
     * analytics events from the Roku app.
     * 
     * @since 5.8.0
     * @return array Analytics configuration
     */
    private static function get_analytics_config() {
        // Check if analytics helpers class exists
        if (!class_exists('CC_Analytics_Helpers')) {
            return array(
                'enabled' => false,
                'reason' => 'Analytics module not available'
            );
        }
        
        // Get the analytics API key (auto-generates if needed)
        $api_key = CC_Analytics_Helpers::get_analytics_key();
        
        // Get current plan for tier-gated features
        $plan = CC_Analytics_Helpers::get_current_plan();
        
        return array(
            'enabled' => true,
            'api_key' => $api_key,
            'ingest_endpoint' => rest_url('castconductor/v5/analytics/ingest'),
            'plan' => $plan,
            'flush_interval_seconds' => 30,
            'batch_size' => 50,
        );
    }
    
    /**
     * Get license configuration for watermark display
     * 
     * Returns watermark settings based on license status:
     * - Trial: Show watermark from day 1
     * - Grace Period: Show watermark with "License Expired" message
     * - Expired: Show watermark with "Renew" call to action
     * - Active (paid): No watermark
     * 
     * @return array License configuration for Roku app
     */
    private static function get_license_config() {
        // Get license manager instance
        $license_manager = castconductor_license_manager();
        
        // Default response for unconfigured/invalid license (show watermark)
        $default_config = array(
            'show_watermark' => true,
            'watermark_reason' => 'trial',
            'watermark_message' => 'CastConductor Trial',
            'watermark_position' => 'upper_third_center',
            'watermark_fallback_position' => 'upper_left_overlay',
            'watermark_animation' => 'fade',
            'watermark_opacity' => 1.0,
        );
        
        // If no license manager available, default to showing watermark
        if (!$license_manager) {
            return $default_config;
        }
        
        // Get current license status
        $license_status = $license_manager->get_license_status();
        
        // No license configured = trial mode
        if (empty($license_status) || $license_status['status'] === 'unconfigured') {
            return $default_config;
        }
        
        // License is valid and active (paid) = no watermark
        if (isset($license_status['valid']) && $license_status['valid'] === true) {
            $status = $license_status['status'] ?? 'active';
            
            // Check if watermark is explicitly forced via features (e.g., trial expired, grace period, test key)
            if (isset($license_status['features']['watermark']) && $license_status['features']['watermark'] === true) {
                // Determine watermark message based on status
                $reason = 'trial';
                $message = 'CastConductor Trial';
                
                if ($status === 'trial') {
                    $reason = 'trial_expired';
                    $message = 'Trial Expired - Renew at castconductor.com';
                } elseif ($status === 'grace_period') {
                    $reason = 'grace_period';
                    $message = 'License Expired';
                }
                
                return array(
                    'show_watermark' => true,
                    'watermark_reason' => $reason,
                    'watermark_message' => $message,
                    'watermark_position' => 'upper_third_center',
                    'watermark_fallback_position' => 'upper_left_overlay',
                    'watermark_animation' => 'fade',
                    'watermark_opacity' => 1.0,
                );
            }
            
            // Active license OR active trial (features.watermark not set or false) = no watermark
            if ($status === 'active' || $status === 'trial') {
                return array(
                    'show_watermark' => false,
                    'watermark_reason' => 'none',
                    'watermark_message' => '',
                    'watermark_position' => 'upper_third_center',
                    'watermark_fallback_position' => 'upper_left_overlay',
                    'watermark_animation' => 'fade',
                    'watermark_opacity' => 1.0,
                );
            }
            
            // Grace period = show watermark with warning
            if ($status === 'grace_period') {
                return array(
                    'show_watermark' => true,
                    'watermark_reason' => 'grace_period',
                    'watermark_message' => 'License Expired',
                    'watermark_position' => 'upper_third_center',
                    'watermark_fallback_position' => 'upper_left_overlay',
                    'watermark_animation' => 'fade',
                    'watermark_opacity' => 1.0,
                );
            }
        }
        
        // Expired license = show watermark with call to action
        if (isset($license_status['status']) && in_array($license_status['status'], array('expired', 'revoked', 'cancelled'))) {
            return array(
                'show_watermark' => true,
                'watermark_reason' => 'expired',
                'watermark_message' => 'Renew at castconductor.com',
                'watermark_position' => 'upper_third_center',
                'watermark_fallback_position' => 'upper_left_overlay',
                'watermark_animation' => 'fade',
                'watermark_opacity' => 1.0,
            );
        }
        
        // Default fallback = show watermark (trial mode)
        return $default_config;
    }
}
