<?php
/**
 * Cast Conductor Proprietary License v5
 * SPDX-License-Identifier: LicenseRef-CastConductor-Proprietary-v5
 * 
 * Copyright (c) 2025 CastConductor.com. All Rights Reserved.
 * See LICENSE file or https://castconductor.com/eula for full terms.
 */

/**
 * Feed Parser - Unified parser for RSS, MRSS, Atom, and JSON feeds
 * 
 * Parses external media feeds and normalizes them to a common data structure
 * for use in feed browser content blocks.
 * 
 * @since 5.6.9
 */

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

class CastConductor_Feed_Parser {
    
    /**
     * Supported feed types
     */
    const FEED_TYPE_RSS = 'rss';
    const FEED_TYPE_MRSS = 'mrss';
    const FEED_TYPE_ATOM = 'atom';
    const FEED_TYPE_JSON = 'json';
    const FEED_TYPE_ROKU = 'roku'; // Roku Direct Publisher format
    const FEED_TYPE_YOUTUBE = 'youtube';
    const FEED_TYPE_SOUNDCLOUD = 'soundcloud';
    
    /**
     * Cache duration in seconds (1 hour default for better performance)
     * Increased from 5 minutes to reduce external API calls and improve
     * reliability for Roku devices loading podcast feeds.
     * 
     * @since 5.7.3
     */
    private $cache_duration = 3600;
    
    /**
     * Thumbnail cache directory relative to wp-content/uploads
     */
    const THUMBNAIL_CACHE_DIR = 'castconductor/feed-thumbnails';
    
    /**
     * Constructor
     * 
     * @param int $cache_duration Cache duration in seconds
     */
    public function __construct($cache_duration = 3600) {
        $this->cache_duration = $cache_duration;
    }
    
    /**
     * Parse a feed URL and return normalized items
     * 
     * @param string $url Feed URL
     * @param string $type Feed type (auto-detect if not specified)
     * @param array $options Additional options (max_items, api_key, etc.)
     * @return array|WP_Error Normalized feed data or error
     */
    public function parse($url, $type = 'auto', $options = array()) {
        // Validate URL
        if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
            return new WP_Error('invalid_url', 'Invalid feed URL provided');
        }
        
        // Check cache first
        $cache_key = 'cc_feed_' . md5($url . serialize($options));
        $cached = get_transient($cache_key);
        if ($cached !== false && empty($options['force_refresh'])) {
            return $cached;
        }
        
        // Auto-detect feed type if not specified
        if ($type === 'auto') {
            $type = $this->detect_feed_type($url);
        }
        
        // Fetch and parse based on type
        $result = null;
        switch ($type) {
            case self::FEED_TYPE_RSS:
            case self::FEED_TYPE_MRSS:
            case self::FEED_TYPE_ATOM:
                $result = $this->parse_xml_feed($url, $type);
                break;
            
            case self::FEED_TYPE_JSON:
            case self::FEED_TYPE_ROKU:
                $result = $this->parse_json_feed($url, $options);
                break;
            
            case self::FEED_TYPE_YOUTUBE:
                $result = $this->parse_youtube_feed($url, $options);
                break;
            
            case self::FEED_TYPE_SOUNDCLOUD:
                $result = $this->parse_soundcloud_feed($url, $options);
                break;
            
            default:
                return new WP_Error('unknown_type', 'Unknown feed type: ' . $type);
        }
        
        if (is_wp_error($result)) {
            return $result;
        }
        
        // Apply max_items limit
        if (!empty($options['max_items']) && is_array($result['items'])) {
            $result['items'] = array_slice($result['items'], 0, (int)$options['max_items']);
        }
        
        // Optimize thumbnail URLs for faster loading (reduce sizes, etc.)
        // This is done server-side to serve optimized URLs to Roku devices
        $this->optimize_feed_thumbnails($result);
        
        // Cache the result
        $cache_duration = !empty($options['cache_duration']) ? (int)$options['cache_duration'] : $this->cache_duration;
        set_transient($cache_key, $result, $cache_duration);
        
        return $result;
    }
    
    /**
     * Auto-detect feed type from URL
     * 
     * @param string $url Feed URL
     * @return string Feed type
     */
    private function detect_feed_type($url) {
        $url_lower = strtolower($url);
        
        // Check for known platforms
        if (strpos($url_lower, 'youtube.com') !== false || strpos($url_lower, 'youtu.be') !== false) {
            return self::FEED_TYPE_YOUTUBE;
        }
        
        // SoundCloud RSS feeds (feeds.soundcloud.com) should be treated as RSS, not SoundCloud API
        // SoundCloud closed their public API in 2017, but RSS feeds work without authentication
        if (strpos($url_lower, 'feeds.soundcloud.com') !== false) {
            return self::FEED_TYPE_SOUNDCLOUD;
        }
        
        // Legacy soundcloud.com URLs that aren't RSS feeds (not currently supported)
        if (strpos($url_lower, 'soundcloud.com') !== false && strpos($url_lower, 'feeds.') === false) {
            return self::FEED_TYPE_SOUNDCLOUD;
        }
        
        // Check file extension
        if (preg_match('/\.(json)$/i', $url)) {
            return self::FEED_TYPE_JSON;
        }
        if (preg_match('/\.(xml|rss|atom)$/i', $url)) {
            return self::FEED_TYPE_RSS;
        }
        
        // Default to RSS (most common for podcast/media feeds)
        return self::FEED_TYPE_RSS;
    }
    
    /**
     * Parse XML-based feeds (RSS, MRSS, Atom)
     * 
     * @param string $url Feed URL
     * @param string $type Feed type
     * @return array|WP_Error Parsed feed data
     */
    private function parse_xml_feed($url, $type) {
        // Fetch feed content
        $response = wp_remote_get($url, array(
            'timeout' => 30,
            'user-agent' => 'CastConductor/5.6 (+https://castconductor.com)',
        ));
        
        if (is_wp_error($response)) {
            return $response;
        }
        
        $body = wp_remote_retrieve_body($response);
        if (empty($body)) {
            return new WP_Error('empty_response', 'Empty response from feed URL');
        }
        
        // Parse XML
        libxml_use_internal_errors(true);
        $xml = simplexml_load_string($body, 'SimpleXMLElement', LIBXML_NOCDATA);
        if ($xml === false) {
            $errors = libxml_get_errors();
            libxml_clear_errors();
            return new WP_Error('xml_parse_error', 'Failed to parse XML: ' . ($errors[0]->message ?? 'Unknown error'));
        }
        
        // Determine actual format and parse
        if (isset($xml->channel)) {
            // RSS 2.0 format
            return $this->parse_rss_channel($xml->channel, $type === self::FEED_TYPE_MRSS);
        } elseif ($xml->getName() === 'feed') {
            // Atom format
            return $this->parse_atom_feed($xml);
        } else {
            return new WP_Error('unknown_format', 'Unrecognized XML feed format');
        }
    }
    
    /**
     * Parse RSS 2.0 channel
     * 
     * @param SimpleXMLElement $channel Channel element
     * @param bool $is_mrss Whether to parse MRSS extensions
     * @return array Parsed feed data
     */
    private function parse_rss_channel($channel, $is_mrss = false) {
        $feed = array(
            'title' => (string)$channel->title,
            'description' => (string)$channel->description,
            'link' => (string)$channel->link,
            'image' => '',
            'type' => $is_mrss ? 'mrss' : 'rss',
            'items' => array(),
        );
        
        // Get channel image
        if (isset($channel->image->url)) {
            $feed['image'] = (string)$channel->image->url;
        }
        
        // iTunes namespace for podcasts
        $itunes = $channel->children('http://www.itunes.com/dtds/podcast-1.0.dtd');
        if (isset($itunes->image)) {
            $attrs = $itunes->image->attributes();
            if (isset($attrs['href'])) {
                $feed['image'] = (string)$attrs['href'];
            }
        }
        
        // Parse items
        foreach ($channel->item as $item) {
            $parsed = $this->parse_rss_item($item, $is_mrss);
            if ($parsed) {
                // Fallback: If item has no thumbnail, use the feed's main image
                // This is common for podcasts where only the channel has artwork
                if (empty($parsed['thumbnail']) && !empty($feed['image'])) {
                    $parsed['thumbnail'] = $feed['image'];
                }
                $feed['items'][] = $parsed;
            }
        }
        
        return $feed;
    }
    
    /**
     * Parse single RSS item
     * 
     * @param SimpleXMLElement $item Item element
     * @param bool $is_mrss Whether to parse MRSS extensions
     * @return array|null Parsed item or null
     */
    private function parse_rss_item($item, $is_mrss = false) {
        $parsed = array(
            'id' => '',
            'title' => (string)$item->title,
            'description' => $this->clean_description((string)$item->description),
            'link' => (string)$item->link,
            'thumbnail' => '',
            'media_url' => '',
            'media_type' => 'audio', // Default to audio for podcasts
            'duration' => 0,
            'published_date' => '',
            'metadata' => array(),
        );
        
        // Get GUID as ID
        if (isset($item->guid)) {
            $parsed['id'] = (string)$item->guid;
        } else {
            $parsed['id'] = md5($parsed['link'] . $parsed['title']);
        }
        
        // Publication date
        if (isset($item->pubDate)) {
            $parsed['published_date'] = date('c', strtotime((string)$item->pubDate));
        }
        
        // Enclosure (standard podcast audio/video)
        if (isset($item->enclosure)) {
            $attrs = $item->enclosure->attributes();
            if (isset($attrs['url'])) {
                $parsed['media_url'] = (string)$attrs['url'];
                if (isset($attrs['type'])) {
                    $type = (string)$attrs['type'];
                    $parsed['media_type'] = strpos($type, 'video') !== false ? 'video' : 'audio';
                }
            }
        }
        
        // iTunes extensions for podcasts
        $itunes = $item->children('http://www.itunes.com/dtds/podcast-1.0.dtd');
        if (isset($itunes->duration)) {
            $parsed['duration'] = $this->parse_duration((string)$itunes->duration);
        }
        if (isset($itunes->image)) {
            $attrs = $itunes->image->attributes();
            if (isset($attrs['href'])) {
                $parsed['thumbnail'] = (string)$attrs['href'];
            }
        }
        if (isset($itunes->author)) {
            $parsed['metadata']['artist'] = (string)$itunes->author;
        }
        if (isset($itunes->episode)) {
            $parsed['metadata']['episode'] = (int)$itunes->episode;
        }
        if (isset($itunes->season)) {
            $parsed['metadata']['season'] = (int)$itunes->season;
        }
        
        // MRSS (Media RSS) extensions
        if ($is_mrss) {
            $media = $item->children('http://search.yahoo.com/mrss/');
            if (isset($media->content)) {
                $attrs = $media->content->attributes();
                if (isset($attrs['url'])) {
                    $parsed['media_url'] = (string)$attrs['url'];
                }
                if (isset($attrs['type'])) {
                    $type = (string)$attrs['type'];
                    $parsed['media_type'] = strpos($type, 'video') !== false ? 'video' : 'audio';
                }
                if (isset($attrs['duration'])) {
                    $parsed['duration'] = (int)$attrs['duration'];
                }
            }
            if (isset($media->thumbnail)) {
                $attrs = $media->thumbnail->attributes();
                if (isset($attrs['url'])) {
                    $parsed['thumbnail'] = (string)$attrs['url'];
                }
            }
            if (isset($media->description)) {
                $parsed['description'] = $this->clean_description((string)$media->description);
            }
        }
        
        return $parsed;
    }
    
    /**
     * Parse Atom feed
     * 
     * @param SimpleXMLElement $xml Feed element
     * @return array Parsed feed data
     */
    private function parse_atom_feed($xml) {
        $ns = $xml->getNamespaces(true);
        
        $feed = array(
            'title' => (string)$xml->title,
            'description' => isset($xml->subtitle) ? (string)$xml->subtitle : '',
            'link' => '',
            'image' => '',
            'type' => 'atom',
            'items' => array(),
        );
        
        // Get link
        foreach ($xml->link as $link) {
            $rel = (string)$link->attributes()->rel;
            if ($rel === 'alternate' || $rel === '') {
                $feed['link'] = (string)$link->attributes()->href;
                break;
            }
        }
        
        // Get logo/icon
        if (isset($xml->logo)) {
            $feed['image'] = (string)$xml->logo;
        } elseif (isset($xml->icon)) {
            $feed['image'] = (string)$xml->icon;
        }
        
        // Parse entries
        foreach ($xml->entry as $entry) {
            $parsed = array(
                'id' => (string)$entry->id,
                'title' => (string)$entry->title,
                'description' => '',
                'link' => '',
                'thumbnail' => '',
                'media_url' => '',
                'media_type' => 'video',
                'duration' => 0,
                'published_date' => '',
                'metadata' => array(),
            );
            
            // Description from summary or content
            if (isset($entry->summary)) {
                $parsed['description'] = $this->clean_description((string)$entry->summary);
            } elseif (isset($entry->content)) {
                $parsed['description'] = $this->clean_description((string)$entry->content);
            }
            
            // Published date
            if (isset($entry->published)) {
                $parsed['published_date'] = (string)$entry->published;
            } elseif (isset($entry->updated)) {
                $parsed['published_date'] = (string)$entry->updated;
            }
            
            // Links
            foreach ($entry->link as $link) {
                $rel = (string)$link->attributes()->rel;
                $type = (string)$link->attributes()->type;
                $href = (string)$link->attributes()->href;
                
                if ($rel === 'alternate' && empty($parsed['link'])) {
                    $parsed['link'] = $href;
                } elseif ($rel === 'enclosure' || strpos($type, 'video') !== false || strpos($type, 'audio') !== false) {
                    $parsed['media_url'] = $href;
                    $parsed['media_type'] = strpos($type, 'video') !== false ? 'video' : 'audio';
                }
            }
            
            // Media RSS in Atom
            if (isset($ns['media'])) {
                $media = $entry->children($ns['media']);
                if (isset($media->thumbnail)) {
                    $parsed['thumbnail'] = (string)$media->thumbnail->attributes()->url;
                }
                if (isset($media->content)) {
                    $attrs = $media->content->attributes();
                    if (isset($attrs['url'])) {
                        $parsed['media_url'] = (string)$attrs['url'];
                    }
                }
            }
            
            // Fallback: If item has no thumbnail, use the feed's main image
            if (empty($parsed['thumbnail']) && !empty($feed['image'])) {
                $parsed['thumbnail'] = $feed['image'];
            }
            
            $feed['items'][] = $parsed;
        }
        
        return $feed;
    }
    
    /**
     * Parse JSON feed
     * 
     * @param string $url Feed URL
     * @param array $options Options including field mappings
     * @return array|WP_Error Parsed feed data
     */
    private function parse_json_feed($url, $options = array()) {
        // Fetch feed content
        $response = wp_remote_get($url, array(
            'timeout' => 30,
            'user-agent' => 'CastConductor/5.6 (+https://castconductor.com)',
        ));
        
        if (is_wp_error($response)) {
            return $response;
        }
        
        $body = wp_remote_retrieve_body($response);
        $data = json_decode($body, true);
        
        if (json_last_error() !== JSON_ERROR_NONE) {
            return new WP_Error('json_parse_error', 'Failed to parse JSON: ' . json_last_error_msg());
        }
        
        // Check for Roku Direct Publisher format
        if (isset($data['providerName']) && isset($data['movies']) || isset($data['series']) || isset($data['shortFormVideos'])) {
            return $this->parse_roku_direct_publisher($data);
        }
        
        // Check for JSON Feed spec (https://jsonfeed.org/)
        if (isset($data['version']) && strpos($data['version'], 'jsonfeed.org') !== false) {
            return $this->parse_json_feed_spec($data);
        }
        
        // Custom JSON with field mappings
        return $this->parse_custom_json($data, $options);
    }
    
    /**
     * Parse Roku Direct Publisher format
     * 
     * @param array $data JSON data
     * @return array Parsed feed data
     */
    private function parse_roku_direct_publisher($data) {
        $feed = array(
            'title' => $data['providerName'] ?? 'Roku Feed',
            'description' => '',
            'link' => '',
            'image' => '',
            'type' => 'roku',
            'items' => array(),
        );
        
        // Collect all video types
        $videos = array();
        if (isset($data['movies'])) {
            $videos = array_merge($videos, $data['movies']);
        }
        if (isset($data['series'])) {
            foreach ($data['series'] as $series) {
                if (isset($series['episodes'])) {
                    foreach ($series['episodes'] as $episode) {
                        $episode['_series'] = $series['title'] ?? '';
                        $videos[] = $episode;
                    }
                }
            }
        }
        if (isset($data['shortFormVideos'])) {
            $videos = array_merge($videos, $data['shortFormVideos']);
        }
        
        // Parse each video
        foreach ($videos as $video) {
            $parsed = array(
                'id' => $video['id'] ?? md5(json_encode($video)),
                'title' => $video['title'] ?? '',
                'description' => $video['shortDescription'] ?? $video['longDescription'] ?? '',
                'link' => '',
                'thumbnail' => $video['thumbnail'] ?? '',
                'media_url' => '',
                'media_type' => 'video',
                'duration' => $video['duration'] ?? 0,
                'published_date' => $video['dateAdded'] ?? $video['releaseDate'] ?? '',
                'metadata' => array(
                    'series' => $video['_series'] ?? '',
                    'season' => $video['seasonNumber'] ?? null,
                    'episode' => $video['episodeNumber'] ?? null,
                    'genres' => $video['genres'] ?? array(),
                    'rating' => $video['rating'] ?? array(),
                ),
            );
            
            // Get content URL from streams
            if (isset($video['content']['videos'])) {
                foreach ($video['content']['videos'] as $stream) {
                    if (isset($stream['url'])) {
                        $parsed['media_url'] = $stream['url'];
                        break;
                    }
                }
            }
            
            $feed['items'][] = $parsed;
        }
        
        return $feed;
    }
    
    /**
     * Parse JSON Feed spec format
     * 
     * @param array $data JSON data
     * @return array Parsed feed data
     */
    private function parse_json_feed_spec($data) {
        $feed = array(
            'title' => $data['title'] ?? '',
            'description' => $data['description'] ?? '',
            'link' => $data['home_page_url'] ?? '',
            'image' => $data['icon'] ?? $data['favicon'] ?? '',
            'type' => 'json',
            'items' => array(),
        );
        
        foreach ($data['items'] ?? array() as $item) {
            $parsed = array(
                'id' => $item['id'] ?? '',
                'title' => $item['title'] ?? '',
                'description' => $this->clean_description($item['content_text'] ?? $item['content_html'] ?? ''),
                'link' => $item['url'] ?? '',
                'thumbnail' => $item['image'] ?? $item['banner_image'] ?? '',
                'media_url' => '',
                'media_type' => 'video',
                'duration' => 0,
                'published_date' => $item['date_published'] ?? '',
                'metadata' => array(
                    'author' => $item['author']['name'] ?? '',
                    'tags' => $item['tags'] ?? array(),
                ),
            );
            
            // Check for attachments
            if (isset($item['attachments'])) {
                foreach ($item['attachments'] as $attachment) {
                    if (isset($attachment['url'])) {
                        $parsed['media_url'] = $attachment['url'];
                        if (isset($attachment['mime_type'])) {
                            $parsed['media_type'] = strpos($attachment['mime_type'], 'video') !== false ? 'video' : 'audio';
                        }
                        if (isset($attachment['duration_in_seconds'])) {
                            $parsed['duration'] = (int)$attachment['duration_in_seconds'];
                        }
                        break;
                    }
                }
            }
            
            $feed['items'][] = $parsed;
        }
        
        return $feed;
    }
    
    /**
     * Parse custom JSON with field mappings
     * 
     * @param array $data JSON data
     * @param array $options Field mapping options
     * @return array Parsed feed data
     */
    private function parse_custom_json($data, $options = array()) {
        $mappings = $options['field_mappings'] ?? array();
        
        $feed = array(
            'title' => $this->get_mapped_field($data, $mappings['feed_title'] ?? 'title', 'Custom Feed'),
            'description' => $this->get_mapped_field($data, $mappings['feed_description'] ?? 'description', ''),
            'link' => $this->get_mapped_field($data, $mappings['feed_link'] ?? 'link', ''),
            'image' => $this->get_mapped_field($data, $mappings['feed_image'] ?? 'image', ''),
            'type' => 'json',
            'items' => array(),
        );
        
        // Get items array
        $items_path = $mappings['items'] ?? 'items';
        $items = $this->get_nested_value($data, $items_path);
        
        if (!is_array($items)) {
            // Maybe the data itself is the items array
            if (isset($data[0])) {
                $items = $data;
            } else {
                return $feed;
            }
        }
        
        foreach ($items as $item) {
            $parsed = array(
                'id' => $this->get_mapped_field($item, $mappings['id'] ?? 'id', md5(json_encode($item))),
                'title' => $this->get_mapped_field($item, $mappings['title'] ?? 'title', ''),
                'description' => $this->clean_description($this->get_mapped_field($item, $mappings['description'] ?? 'description', '')),
                'link' => $this->get_mapped_field($item, $mappings['link'] ?? 'link', ''),
                'thumbnail' => $this->get_mapped_field($item, $mappings['thumbnail'] ?? 'thumbnail', ''),
                'media_url' => $this->get_mapped_field($item, $mappings['media_url'] ?? 'media_url', ''),
                'media_type' => $this->get_mapped_field($item, $mappings['media_type'] ?? 'media_type', 'video'),
                'duration' => (int)$this->get_mapped_field($item, $mappings['duration'] ?? 'duration', 0),
                'published_date' => $this->get_mapped_field($item, $mappings['published_date'] ?? 'published_date', ''),
                'metadata' => array(),
            );
            
            $feed['items'][] = $parsed;
        }
        
        return $feed;
    }
    
    /**
     * Parse YouTube feed (playlist or channel)
     * 
     * @param string $url YouTube URL
     * @param array $options Options including API key
     * @return array|WP_Error Parsed feed data
     */
    private function parse_youtube_feed($url, $options = array()) {
        // Check for YouTube RSS feed format first (no API key required!)
        // YouTube provides RSS feeds at:
        //   - Channel: https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID
        //   - Playlist: https://www.youtube.com/feeds/videos.xml?playlist_id=PLAYLIST_ID
        if (strpos($url, '/feeds/videos.xml') !== false) {
            return $this->parse_youtube_rss_feed($url);
        }
        
        // Check if we can convert a regular YouTube URL to RSS format
        $rss_url = $this->convert_youtube_url_to_rss($url);
        if ($rss_url) {
            return $this->parse_youtube_rss_feed($rss_url);
        }
        
        // Fall back to YouTube API (requires API key)
        $api_key = $options['api_key'] ?? get_option('castconductor_youtube_api_key', '');
        
        if (empty($api_key)) {
            return new WP_Error(
                'missing_api_key', 
                'YouTube API key required for this URL format. ' .
                'Alternatively, use the RSS feed URL: ' .
                'https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID'
            );
        }
        
        // Extract playlist or channel ID from URL
        $playlist_id = null;
        $channel_id = null;
        
        if (preg_match('/[?&]list=([a-zA-Z0-9_-]+)/', $url, $matches)) {
            $playlist_id = $matches[1];
        } elseif (preg_match('/\/channel\/([a-zA-Z0-9_-]+)/', $url, $matches)) {
            $channel_id = $matches[1];
        } elseif (preg_match('/\/@([a-zA-Z0-9_-]+)/', $url, $matches)) {
            // Handle @username format - would need extra API call to resolve
            return new WP_Error('handle_format', 'YouTube @handle URLs not yet supported. Use channel ID or playlist URL.');
        }
        
        if ($playlist_id) {
            return $this->fetch_youtube_playlist($playlist_id, $api_key, $options);
        } elseif ($channel_id) {
            return $this->fetch_youtube_channel_uploads($channel_id, $api_key, $options);
        }
        
        return new WP_Error('invalid_url', 'Could not extract YouTube playlist or channel ID from URL');
    }
    
    /**
     * Convert YouTube URL to RSS feed URL
     * 
     * @param string $url YouTube URL
     * @return string|null RSS feed URL or null if conversion not possible
     */
    private function convert_youtube_url_to_rss($url) {
        // Extract channel ID from /channel/CHANNEL_ID format
        if (preg_match('/\/channel\/([a-zA-Z0-9_-]+)/', $url, $matches)) {
            return 'https://www.youtube.com/feeds/videos.xml?channel_id=' . $matches[1];
        }
        
        // Extract playlist ID from ?list=PLAYLIST_ID format
        if (preg_match('/[?&]list=([a-zA-Z0-9_-]+)/', $url, $matches)) {
            return 'https://www.youtube.com/feeds/videos.xml?playlist_id=' . $matches[1];
        }
        
        return null;
    }
    
    /**
     * Parse YouTube RSS feed (Atom format)
     * No API key required!
     * 
     * YouTube RSS feeds return Atom XML with media extensions.
     * 
     * @param string $url YouTube RSS feed URL
     * @return array|WP_Error Parsed feed data
     */
    private function parse_youtube_rss_feed($url) {
        $response = wp_remote_get($url, array(
            'timeout' => 30,
            'user-agent' => 'CastConductor/5.6 (+https://castconductor.com)',
        ));
        
        if (is_wp_error($response)) {
            return $response;
        }
        
        $body = wp_remote_retrieve_body($response);
        if (empty($body)) {
            return new WP_Error('empty_response', 'Empty response from YouTube RSS feed');
        }
        
        // Parse Atom XML
        libxml_use_internal_errors(true);
        $xml = simplexml_load_string($body, 'SimpleXMLElement', LIBXML_NOCDATA);
        if ($xml === false) {
            $errors = libxml_get_errors();
            libxml_clear_errors();
            return new WP_Error('xml_parse_error', 'Failed to parse YouTube RSS: ' . ($errors[0]->message ?? 'Unknown error'));
        }
        
        // Register namespaces
        $namespaces = $xml->getNamespaces(true);
        $yt = isset($namespaces['yt']) ? $xml->children($namespaces['yt']) : null;
        $media = isset($namespaces['media']) ? $namespaces['media'] : 'http://search.yahoo.com/mrss/';
        
        $feed = array(
            'title' => (string)$xml->title,
            'description' => '',
            'link' => '',
            'image' => '',
            'type' => 'youtube_rss',
            'source' => 'youtube_rss',
            'items' => array(),
        );
        
        // Get channel link
        foreach ($xml->link as $link) {
            $rel = (string)$link->attributes()->rel;
            if ($rel === 'alternate') {
                $feed['link'] = (string)$link->attributes()->href;
                break;
            }
        }
        
        // Parse entries
        foreach ($xml->entry as $entry) {
            $videoId = '';
            $ytChildren = $entry->children('http://www.youtube.com/xml/schemas/2015');
            if (isset($ytChildren->videoId)) {
                $videoId = (string)$ytChildren->videoId;
            }
            
            // Get media:group for thumbnail
            $thumbnail = '';
            $mediaGroup = $entry->children($media);
            if (isset($mediaGroup->group->thumbnail)) {
                $thumbnail = (string)$mediaGroup->group->thumbnail->attributes()->url;
            }
            
            // YouTube RSS entries
            $parsed = array(
                'id' => $videoId,
                'title' => (string)$entry->title,
                'description' => '',
                'link' => 'https://www.youtube.com/watch?v=' . $videoId,
                'thumbnail' => $thumbnail ?: 'https://img.youtube.com/vi/' . $videoId . '/hqdefault.jpg',
                'media_url' => 'https://www.youtube.com/watch?v=' . $videoId,
                'media_type' => 'video',
                'duration' => 0, // YouTube RSS doesn't include duration
                'published_date' => (string)$entry->published,
                'metadata' => array(
                    'channel' => (string)$entry->author->name,
                    'channel_url' => (string)$entry->author->uri,
                ),
            );
            
            // Get description from media:group if available
            if (isset($mediaGroup->group->description)) {
                $parsed['description'] = $this->clean_description((string)$mediaGroup->group->description);
            }
            
            $feed['items'][] = $parsed;
        }
        
        // Update feed title to include channel name
        if (!empty($feed['items']) && !empty($feed['items'][0]['metadata']['channel'])) {
            $feed['description'] = 'Videos from ' . $feed['items'][0]['metadata']['channel'];
        }
        
        return $feed;
    }
    
    /**
     * Fetch YouTube playlist videos
     * 
     * @param string $playlist_id Playlist ID
     * @param string $api_key YouTube API key
     * @param array $options Options
     * @return array|WP_Error Parsed feed data
     */
    private function fetch_youtube_playlist($playlist_id, $api_key, $options = array()) {
        $max_results = min($options['max_items'] ?? 50, 50);
        
        $api_url = add_query_arg(array(
            'part' => 'snippet,contentDetails',
            'playlistId' => $playlist_id,
            'maxResults' => $max_results,
            'key' => $api_key,
        ), 'https://www.googleapis.com/youtube/v3/playlistItems');
        
        $response = wp_remote_get($api_url, array('timeout' => 30));
        
        if (is_wp_error($response)) {
            return $response;
        }
        
        $data = json_decode(wp_remote_retrieve_body($response), true);
        
        if (isset($data['error'])) {
            return new WP_Error('youtube_api_error', $data['error']['message'] ?? 'YouTube API error');
        }
        
        $feed = array(
            'title' => 'YouTube Playlist',
            'description' => '',
            'link' => 'https://www.youtube.com/playlist?list=' . $playlist_id,
            'image' => '',
            'type' => 'youtube',
            'items' => array(),
        );
        
        foreach ($data['items'] ?? array() as $item) {
            $snippet = $item['snippet'] ?? array();
            $video_id = $snippet['resourceId']['videoId'] ?? '';
            
            $parsed = array(
                'id' => $video_id,
                'title' => $snippet['title'] ?? '',
                'description' => $this->clean_description($snippet['description'] ?? ''),
                'link' => 'https://www.youtube.com/watch?v=' . $video_id,
                'thumbnail' => $snippet['thumbnails']['high']['url'] ?? $snippet['thumbnails']['default']['url'] ?? '',
                'media_url' => 'https://www.youtube.com/watch?v=' . $video_id, // YouTube requires embed
                'media_type' => 'video',
                'duration' => 0, // Would need video details API call
                'published_date' => $snippet['publishedAt'] ?? '',
                'metadata' => array(
                    'channel' => $snippet['channelTitle'] ?? '',
                    'position' => $item['snippet']['position'] ?? 0,
                ),
            );
            
            $feed['items'][] = $parsed;
        }
        
        // Update feed title from first item's channel
        if (!empty($feed['items']) && !empty($feed['items'][0]['metadata']['channel'])) {
            $feed['title'] = $feed['items'][0]['metadata']['channel'] . ' - Playlist';
        }
        
        return $feed;
    }
    
    /**
     * Fetch YouTube channel uploads
     * 
     * @param string $channel_id Channel ID
     * @param string $api_key YouTube API key
     * @param array $options Options
     * @return array|WP_Error Parsed feed data
     */
    private function fetch_youtube_channel_uploads($channel_id, $api_key, $options = array()) {
        // First get the uploads playlist ID for this channel
        $channel_url = add_query_arg(array(
            'part' => 'contentDetails,snippet',
            'id' => $channel_id,
            'key' => $api_key,
        ), 'https://www.googleapis.com/youtube/v3/channels');
        
        $response = wp_remote_get($channel_url, array('timeout' => 30));
        
        if (is_wp_error($response)) {
            return $response;
        }
        
        $data = json_decode(wp_remote_retrieve_body($response), true);
        
        if (isset($data['error'])) {
            return new WP_Error('youtube_api_error', $data['error']['message'] ?? 'YouTube API error');
        }
        
        if (empty($data['items'])) {
            return new WP_Error('channel_not_found', 'YouTube channel not found');
        }
        
        $uploads_playlist_id = $data['items'][0]['contentDetails']['relatedPlaylists']['uploads'] ?? null;
        
        if (!$uploads_playlist_id) {
            return new WP_Error('no_uploads', 'Could not find uploads playlist for channel');
        }
        
        // Now fetch the playlist
        $feed = $this->fetch_youtube_playlist($uploads_playlist_id, $api_key, $options);
        
        if (!is_wp_error($feed)) {
            $feed['title'] = $data['items'][0]['snippet']['title'] ?? 'YouTube Channel';
            $feed['description'] = $data['items'][0]['snippet']['description'] ?? '';
            $feed['image'] = $data['items'][0]['snippet']['thumbnails']['high']['url'] ?? '';
            $feed['link'] = 'https://www.youtube.com/channel/' . $channel_id;
        }
        
        return $feed;
    }
    
    /**
     * Parse SoundCloud feed
     * 
     * SoundCloud provides RSS feeds for users and playlists that work without API authentication.
     * Feed URLs: 
     *   - User: https://feeds.soundcloud.com/users/soundcloud:users:{USER_ID}/sounds.rss
     *   - Playlist: https://feeds.soundcloud.com/playlists/soundcloud:playlists:{PLAYLIST_ID}/sounds.rss
     * 
     * These feeds include iTunes extensions for high-quality artwork, duration, etc.
     * 
     * @param string $url SoundCloud URL
     * @param array $options Options
     * @return array|WP_Error Parsed feed data
     */
    private function parse_soundcloud_feed($url, $options = array()) {
        // Check if this is a SoundCloud RSS feed URL
        if (strpos(strtolower($url), 'feeds.soundcloud.com') !== false) {
            // SoundCloud RSS feeds are standard RSS 2.0 with iTunes extensions
            // Delegate to the RSS parser which already handles iTunes namespace
            $result = $this->parse_xml_feed($url, self::FEED_TYPE_RSS);
            
            if (!is_wp_error($result)) {
                // Mark as SoundCloud type for UI purposes
                $result['type'] = 'soundcloud';
                $result['source'] = 'soundcloud_rss';
                
                // SoundCloud RSS uses itunes:image (up to 3000x3000) for artwork
                // and enclosure for direct audio stream URLs
                // All already parsed by parse_rss_item() via iTunes namespace handling
            }
            
            return $result;
        }
        
        // Try to auto-convert user-friendly SoundCloud URLs to RSS feed URLs
        $page_body = null; // Cache page content for fallback
        $rss_url = $this->soundcloud_url_to_rss($url, $page_body);
        
        if ($rss_url) {
            // Successfully discovered RSS feed URL, parse it
            $result = $this->parse_xml_feed($rss_url, self::FEED_TYPE_RSS);
            
            if (!is_wp_error($result)) {
                $result['type'] = 'soundcloud';
                $result['source'] = 'soundcloud_rss';
                $result['original_url'] = $url;
                $result['discovered_rss'] = $rss_url;
                
                // Check if RSS returned 0 items - fall back to page scraping for playlists
                if (empty($result['items']) && strpos(strtolower($url), '/sets/') !== false) {
                    $scraped = $this->parse_soundcloud_page($url, $page_body);
                    if (!is_wp_error($scraped) && !empty($scraped['items'])) {
                        $scraped['source'] = 'soundcloud_page_scrape';
                        $scraped['original_url'] = $url;
                        $scraped['rss_was_empty'] = true;
                        return $scraped;
                    }
                }
            }
            
            return $result;
        }
        
        // Could not auto-discover RSS URL - try page scraping as last resort for playlists
        if (strpos(strtolower($url), '/sets/') !== false) {
            $scraped = $this->parse_soundcloud_page($url, $page_body);
            if (!is_wp_error($scraped) && !empty($scraped['items'])) {
                return $scraped;
            }
        }
        
        // Could not get data from any source
        return new WP_Error(
            'soundcloud_parse_failed',
            'Could not parse this SoundCloud URL. The playlist RSS feed may be unavailable or the page structure has changed.'
        );
    }
    
    /**
     * Auto-convert SoundCloud web URLs to RSS feed URLs
     * 
     * Fetches the SoundCloud page and extracts the playlist/user ID from
     * the embedded JSON data to construct the RSS feed URL.
     * 
     * @param string $url SoundCloud web URL (e.g., https://soundcloud.com/acid877/sets/hollys-house)
     * @param string|null &$page_body Reference to store page body for reuse in fallback
     * @return string|false RSS feed URL or false if not discoverable
     */
    private function soundcloud_url_to_rss($url, &$page_body = null) {
        $url_lower = strtolower($url);
        
        // Only process soundcloud.com URLs
        if (strpos($url_lower, 'soundcloud.com') === false) {
            return false;
        }
        
        // Fetch the page content
        $response = wp_remote_get($url, array(
            'timeout' => 10,
            'user-agent' => 'CastConductor/5.0 (+https://castconductor.com)',
            'redirection' => 3
        ));
        
        if (is_wp_error($response)) {
            return false;
        }
        
        $body = wp_remote_retrieve_body($response);
        if (empty($body)) {
            return false;
        }
        
        // Store page body for potential fallback use
        $page_body = $body;
        
        // Determine if this is a playlist (sets/) or user profile
        $is_playlist = strpos($url_lower, '/sets/') !== false;
        
        if ($is_playlist) {
            // Look for playlist data in the page JSON
            // Pattern: "playlist"...{"id":492618351}
            if (preg_match('/"playlist"[^{]*\{[^}]*"id"\s*:\s*(\d+)/', $body, $matches)) {
                $playlist_id = $matches[1];
                return "https://feeds.soundcloud.com/playlists/soundcloud:playlists:{$playlist_id}/sounds.rss";
            }
        } else {
            // Look for user ID
            // Pattern: "urn":"soundcloud:users:2841813"
            if (preg_match('/"urn"\s*:\s*"soundcloud:users:(\d+)"/', $body, $matches)) {
                $user_id = $matches[1];
                return "https://feeds.soundcloud.com/users/soundcloud:users:{$user_id}/sounds.rss";
            }
        }
        
        return false;
    }
    
    /**
     * Parse SoundCloud playlist page by scraping embedded JSON data
     * 
     * Fallback method when RSS feed is empty/unavailable. Extracts track data
     * from the __sc_hydration JSON embedded in the page HTML.
     * 
     * This method handles SoundCloud's lazy loading by:
     * 1. Extracting hydration data with full track info for initially visible tracks
     * 2. For stub tracks (id + policy only), fetching additional data via API
     * 3. Using SoundCloud's resolve API to get stream URLs
     * 
     * @param string $url SoundCloud playlist URL
     * @param string|null $page_body Pre-fetched page body (optional)
     * @return array|WP_Error Parsed feed data
     */
    private function parse_soundcloud_page($url, $page_body = null) {
        // Fetch page if not already provided
        if (empty($page_body)) {
            $response = wp_remote_get($url, array(
                'timeout' => 15,
                'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                'redirection' => 3
            ));
            
            if (is_wp_error($response)) {
                return $response;
            }
            
            $page_body = wp_remote_retrieve_body($response);
        }
        
        if (empty($page_body)) {
            return new WP_Error('empty_response', 'Empty response from SoundCloud');
        }
        
        // Extract client_id from the page scripts (needed for API calls)
        $client_id = $this->extract_soundcloud_client_id($page_body);
        
        // Extract the __sc_hydration JSON data
        // Pattern: window.__sc_hydration = [{...}];
        if (!preg_match('/window\.__sc_hydration\s*=\s*(\[.*?\]);/s', $page_body, $matches)) {
            return new WP_Error('no_hydration_data', 'Could not find SoundCloud hydration data');
        }
        
        $hydration_json = $matches[1];
        $hydration = json_decode($hydration_json, true);
        
        if (!is_array($hydration)) {
            return new WP_Error('invalid_json', 'Could not parse SoundCloud hydration data');
        }
        
        // Find the playlist data in hydration array
        $playlist_data = null;
        foreach ($hydration as $item) {
            if (isset($item['hydratable']) && $item['hydratable'] === 'playlist' && isset($item['data'])) {
                $playlist_data = $item['data'];
                break;
            }
        }
        
        if (!$playlist_data || empty($playlist_data['tracks'])) {
            return new WP_Error('no_playlist_data', 'Could not find playlist track data');
        }
        
        // Collect track IDs that need full data (stub tracks)
        $stub_track_ids = array();
        $full_tracks = array();
        
        foreach ($playlist_data['tracks'] as $track) {
            if (isset($track['title']) && !empty($track['title'])) {
                // Full track data available
                $full_tracks[$track['id']] = $track;
            } elseif (isset($track['id'])) {
                // Stub track - needs fetching
                $stub_track_ids[] = $track['id'];
            }
        }
        
        // Fetch full data for stub tracks if we have a client_id
        if (!empty($stub_track_ids) && !empty($client_id)) {
            $fetched_tracks = $this->fetch_soundcloud_tracks($stub_track_ids, $client_id);
            foreach ($fetched_tracks as $track) {
                if (isset($track['id'])) {
                    $full_tracks[$track['id']] = $track;
                }
            }
        }
        
        // Build normalized feed result, preserving playlist order
        $items = array();
        foreach ($playlist_data['tracks'] as $track) {
            $track_id = $track['id'] ?? null;
            if (!$track_id) continue;
            
            // Get the full track data (either from hydration or fetched)
            $full_track = $full_tracks[$track_id] ?? null;
            if (!$full_track || empty($full_track['title'])) continue;
            
            // Get stream URL from media transcodings
            $stream_url = $this->get_soundcloud_stream_url($full_track, $client_id);
            
            // Get artwork URL (use large size, replace -large with -t500x500 for better quality)
            $artwork = '';
            if (!empty($full_track['artwork_url'])) {
                $artwork = str_replace('-large.', '-t500x500.', $full_track['artwork_url']);
            } elseif (!empty($full_track['user']['avatar_url'])) {
                $artwork = str_replace('-large.', '-t500x500.', $full_track['user']['avatar_url']);
            }
            
            $items[] = array(
                'id' => 'soundcloud_' . $track_id,
                'title' => $full_track['title'] ?? '',
                'description' => $this->clean_description($full_track['description'] ?? ''),
                'thumbnail' => $artwork,
                'media_url' => $stream_url,
                'media_type' => 'audio',
                'duration' => isset($full_track['duration']) ? intval($full_track['duration'] / 1000) : 0,
                'published_date' => $full_track['created_at'] ?? '',
                'link' => $full_track['permalink_url'] ?? '',
                'author' => $full_track['user']['username'] ?? $playlist_data['user']['username'] ?? '',
            );
        }
        
        if (empty($items)) {
            return new WP_Error('no_tracks', 'No playable tracks found in playlist');
        }
        
        // Get playlist artwork
        $playlist_artwork = '';
        if (!empty($playlist_data['artwork_url'])) {
            $playlist_artwork = str_replace('-large.', '-t500x500.', $playlist_data['artwork_url']);
        } elseif (!empty($items[0]['thumbnail'])) {
            $playlist_artwork = $items[0]['thumbnail'];
        }
        
        return array(
            'title' => $playlist_data['title'] ?? 'SoundCloud Playlist',
            'description' => $this->clean_description($playlist_data['description'] ?? ''),
            'link' => $playlist_data['permalink_url'] ?? $url,
            'image' => $playlist_artwork,
            'type' => 'soundcloud',
            'source' => 'soundcloud_page_scrape',
            'original_url' => $url,
            'items' => $items,
        );
    }
    
    /**
     * Extract SoundCloud client_id from page scripts
     * 
     * The client_id is typically embedded in one of the JavaScript bundle files
     * that SoundCloud loads. We need to fetch and parse these to find it.
     * 
     * @param string $page_body HTML page content
     * @return string|null Client ID or null if not found
     */
    private function extract_soundcloud_client_id($page_body) {
        // Patterns that match client_id in various formats
        // The ID is typically 32 alphanumeric characters
        $patterns = array(
            '/client_id["\']?\s*[=:]\s*["\']([a-zA-Z0-9]{28,40})["\']/',
            '/clientId["\']?\s*[=:]\s*["\']([a-zA-Z0-9]{28,40})["\']/',
            '/"client_id"\s*:\s*"([a-zA-Z0-9]{28,40})"/',
        );
        
        // First, try to find in the page HTML itself (sometimes inlined)
        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $page_body, $matches)) {
                return $matches[1];
            }
        }
        
        // Find ALL script tags with sndcdn sources (SoundCloud CDN)
        // The client_id is usually in one of the main bundle JS files
        preg_match_all('/<script[^>]+src="([^"]*sndcdn[^"]*\.js[^"]*)"/', $page_body, $script_matches);
        
        if (!empty($script_matches[1])) {
            // Check each JS file until we find the client_id
            foreach ($script_matches[1] as $script_url) {
                $script_response = wp_remote_get($script_url, array(
                    'timeout' => 5,
                    'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                ));
                
                if (!is_wp_error($script_response)) {
                    $script_body = wp_remote_retrieve_body($script_response);
                    foreach ($patterns as $pattern) {
                        if (preg_match($pattern, $script_body, $matches)) {
                            return $matches[1];
                        }
                    }
                }
            }
        }
        
        return null;
    }
    
    /**
     * Fetch full track data for multiple track IDs from SoundCloud API
     * 
     * @param array $track_ids Array of track IDs
     * @param string $client_id SoundCloud client ID
     * @return array Array of track data objects
     */
    private function fetch_soundcloud_tracks($track_ids, $client_id) {
        if (empty($track_ids) || empty($client_id)) {
            return array();
        }
        
        // SoundCloud API allows fetching multiple tracks at once
        $ids_param = implode(',', array_slice($track_ids, 0, 50)); // Limit to 50 per request
        $api_url = "https://api-v2.soundcloud.com/tracks?ids={$ids_param}&client_id={$client_id}";
        
        $response = wp_remote_get($api_url, array(
            'timeout' => 10,
            'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            'headers' => array(
                'Accept' => 'application/json',
            ),
        ));
        
        if (is_wp_error($response)) {
            return array();
        }
        
        $body = wp_remote_retrieve_body($response);
        $tracks = json_decode($body, true);
        
        if (!is_array($tracks)) {
            return array();
        }
        
        return $tracks;
    }
    
    /**
     * Get playable stream URL for a SoundCloud track
     * 
     * Resolves the transcoding API URL to get the actual stream URL.
     * 
     * @param array $track Track data with media transcodings
     * @param string $client_id SoundCloud client ID
     * @return string Stream URL or empty string
     */
    private function get_soundcloud_stream_url($track, $client_id) {
        if (empty($track['media']['transcodings']) || empty($client_id)) {
            return '';
        }
        
        // Find the best transcoding (prefer progressive MP3)
        $transcoding_url = '';
        foreach ($track['media']['transcodings'] as $transcoding) {
            if (isset($transcoding['format']['protocol']) && 
                $transcoding['format']['protocol'] === 'progressive' &&
                isset($transcoding['format']['mime_type']) &&
                strpos($transcoding['format']['mime_type'], 'audio/mpeg') !== false) {
                $transcoding_url = $transcoding['url'];
                break;
            }
        }
        
        // Fall back to any progressive format
        if (empty($transcoding_url)) {
            foreach ($track['media']['transcodings'] as $transcoding) {
                if (isset($transcoding['format']['protocol']) && 
                    $transcoding['format']['protocol'] === 'progressive') {
                    $transcoding_url = $transcoding['url'];
                    break;
                }
            }
        }
        
        // Fall back to HLS if no progressive available
        if (empty($transcoding_url) && !empty($track['media']['transcodings'][0]['url'])) {
            $transcoding_url = $track['media']['transcodings'][0]['url'];
        }
        
        if (empty($transcoding_url)) {
            return '';
        }
        
        // Resolve the transcoding URL to get the actual stream URL
        $resolve_url = $transcoding_url . '?client_id=' . $client_id;
        
        $response = wp_remote_get($resolve_url, array(
            'timeout' => 5,
            'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            'headers' => array(
                'Accept' => 'application/json',
            ),
        ));
        
        if (is_wp_error($response)) {
            return '';
        }
        
        $body = wp_remote_retrieve_body($response);
        $data = json_decode($body, true);
        
        if (isset($data['url'])) {
            return $data['url'];
        }
        
        return '';
    }
    
    /**
     * Clean HTML from description text
     * 
     * @param string $text Description text
     * @return string Cleaned text
     */
    private function clean_description($text) {
        // Strip HTML tags
        $text = strip_tags($text);
        // Decode HTML entities
        $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        // Normalize whitespace
        $text = preg_replace('/\s+/', ' ', $text);
        // Trim
        return trim($text);
    }
    
    /**
     * Parse duration string to seconds
     * 
     * @param string $duration Duration string (HH:MM:SS or seconds)
     * @return int Duration in seconds
     */
    private function parse_duration($duration) {
        // If already numeric, return as-is
        if (is_numeric($duration)) {
            return (int)$duration;
        }
        
        // Parse HH:MM:SS or MM:SS format
        $parts = array_reverse(explode(':', $duration));
        $seconds = 0;
        
        if (isset($parts[0])) $seconds += (int)$parts[0];
        if (isset($parts[1])) $seconds += (int)$parts[1] * 60;
        if (isset($parts[2])) $seconds += (int)$parts[2] * 3600;
        
        return $seconds;
    }
    
    /**
     * Get nested value from array using dot notation
     * 
     * @param array $array Source array
     * @param string $path Dot-notation path (e.g., "data.items")
     * @return mixed Value or null
     */
    private function get_nested_value($array, $path) {
        $keys = explode('.', $path);
        $value = $array;
        
        foreach ($keys as $key) {
            if (!is_array($value) || !isset($value[$key])) {
                return null;
            }
            $value = $value[$key];
        }
        
        return $value;
    }
    
    /**
     * Get mapped field value from item
     * 
     * @param array $item Source item
     * @param string $path Field path (dot notation supported)
     * @param mixed $default Default value
     * @return mixed Field value
     */
    private function get_mapped_field($item, $path, $default = '') {
        $value = $this->get_nested_value($item, $path);
        return $value !== null ? $value : $default;
    }
    
    /**
     * Clear feed cache for a URL
     * 
     * @param string $url Feed URL
     * @param array $options Options used for cache key
     */
    public function clear_cache($url, $options = array()) {
        $cache_key = 'cc_feed_' . md5($url . serialize($options));
        delete_transient($cache_key);
    }
    
    /**
     * Clear all feed caches
     */
    public function clear_all_caches() {
        global $wpdb;
        $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_cc_feed_%'");
        $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_cc_feed_%'");
    }
    
    /**
     * Optimize thumbnail URL for faster loading
     * 
     * Applies platform-specific optimizations to reduce image sizes:
     * - SoundCloud: t3000x3000 → t500x500 (500KB → ~30KB)
     * - Anchor/Spotify: Same technique
     * - Other platforms: Pass through unchanged
     * 
     * @since 5.7.3
     * @param string $url Original thumbnail URL
     * @return string Optimized thumbnail URL
     */
    public function optimize_thumbnail_url($url) {
        if (empty($url)) {
            return $url;
        }
        
        // SoundCloud and Anchor.fm use i1.sndcdn.com with size parameters
        // Available sizes: t120x120, t200x200, t300x300, t500x500, t3000x3000
        // t500x500 is ideal for Roku (120x120 display with retina support)
        if (strpos($url, 'sndcdn.com') !== false || 
            strpos($url, 'soundcloud.com') !== false ||
            strpos($url, 'anchor.fm') !== false) {
            // Replace high-res with optimal size
            $optimized = preg_replace('/\-t\d+x\d+\./', '-t500x500.', $url);
            if ($optimized !== $url) {
                return $optimized;
            }
        }
        
        // Spotify CDN images - mosaic format
        // Format: https://mosaic.scdn.co/640/... or /300/
        if (strpos($url, 'scdn.co') !== false) {
            // 300 is optimal for Roku, reduce from 640
            $optimized = str_replace('/640/', '/300/', $url);
            if ($optimized !== $url) {
                return $optimized;
            }
        }
        
        // Podbean images
        if (strpos($url, 'podbean.com') !== false) {
            // Podbean uses -logo300.jpg format, ensure we use 300
            $optimized = preg_replace('/-logo\d+\./', '-logo300.', $url);
            if ($optimized !== $url) {
                return $optimized;
            }
        }
        
        // Libsyn images - they use /height/width/ path format
        if (strpos($url, 'libsyn.com') !== false) {
            $optimized = preg_replace('/\/\d+\/\d+\//', '/500/500/', $url);
            if ($optimized !== $url) {
                return $optimized;
            }
        }
        
        // Blubrry images
        if (strpos($url, 'blubrry.com') !== false) {
            // Blubrry allows ?w=500 parameter
            if (strpos($url, '?') === false) {
                return $url . '?w=500&h=500';
            }
        }
        
        // Transistor.fm images
        if (strpos($url, 'transistor.fm') !== false || strpos($url, 'images.transistor.fm') !== false) {
            // Transistor uses Imgix, supports ?w=500 parameters
            if (strpos($url, '?') === false) {
                return $url . '?w=500&h=500&fit=crop';
            }
        }
        
        // Return original URL if no optimization possible
        return $url;
    }
    
    /**
     * Process feed items to optimize thumbnail URLs
     * 
     * @since 5.7.3
     * @param array $result Feed result from parse()
     * @return array Feed result with optimized thumbnails
     */
    public function optimize_feed_thumbnails(&$result) {
        if (!is_array($result) || !isset($result['items'])) {
            return $result;
        }
        
        foreach ($result['items'] as &$item) {
            if (!empty($item['thumbnail'])) {
                $item['thumbnail'] = $this->optimize_thumbnail_url($item['thumbnail']);
            }
        }
        
        // Also optimize the feed image
        if (!empty($result['image'])) {
            $result['image'] = $this->optimize_thumbnail_url($result['image']);
        }
        
        return $result;
    }
}
