<?php

/**
 * RSS Feed.
 *
 * @link    https://aiopodcasting.com
 * @since   1.0.0
 *
 * @package AiO_Podcast_Studio
 */

// Exit if accessed directly
if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * AIOPOD_Public_Feed class.
 *
 * @since 1.0.0
 */
class AIOPOD_Public_Feed {	
	
	/**
	 * Template redirect handler.
	 *
	 * @since 1.0.0
	 */
	public function template_redirect() {
		// Never run in admin or AJAX context
		if ( is_admin() || wp_doing_ajax() ) {
			return;
		}

		$feed_type    = get_query_var( 'aiopod_feed' );
		$podcast_slug = get_query_var( 'aiopod_podcast' );

		if ( 'podcast' !== $feed_type || ! $podcast_slug ) {
			return;
		}

		$podcast = get_page_by_path( $podcast_slug, OBJECT, 'aiopod_podcasts' );
		$is_blocked_or_invalid = 0;

		if ( ! $podcast ) {
			$is_blocked_or_invalid = 1;			
		} else {
			$podcast_id = (int) $podcast->ID;
			$is_blocked_or_invalid = get_post_meta( $podcast_id, 'is_blocked', true );
		}

		// Invalid podcast feed → proper 404
		if ( $is_blocked_or_invalid ) {
			global $wp_query;
			$wp_query->set_404();
			status_header( 404 );
			nocache_headers();
			return;
		}

		// Valid podcast feed
		status_header( 200 );
		nocache_headers();
		header( 'Content-Type: application/rss+xml; charset=UTF-8' );

		$transient_key = 'aiopod_feed_' . $podcast_id;
		$cache_enabled = (bool) apply_filters( 'aiopod_feed_transient_enabled', true, $podcast_id );
		$cached_output = $cache_enabled ? get_transient( $transient_key ) : false;

		if ( false !== $cached_output ) {
			// Decompress if the cached value was stored with gzcompress.
			// Falls back to the raw value for plain-text entries or if zlib
			// is unavailable on this server.
			if ( function_exists( 'gzuncompress' ) ) {
				$decompressed = @gzuncompress( $cached_output );

				if ( false !== $decompressed ) {
					$cached_output = $decompressed;
				}
			}

			echo $cached_output; // phpcs:ignore WordPress.Security.EscapeOutput
		} else {
			ob_start();
			$this->render_feed( $podcast );
			$output = ob_get_clean();

			if ( $cache_enabled ) {
				// Compress before caching to reduce the stored size and avoid
				// max_allowed_packet errors on large feeds. Plain storage is used
				// if the zlib extension is unavailable. If the transient write
				// fails for any reason the feed is already buffered and will be
				// served normally — the next request will simply re-render.
				$to_cache = $output;

				if ( function_exists( 'gzcompress' ) ) {
					$compressed = gzcompress( $output );

					if ( false !== $compressed ) {
						$to_cache = $compressed;
					}
				}

				// Suppress DB error logging for this write — a failure (e.g.
				// max_allowed_packet) is non-fatal and would otherwise flood
				// debug.log. The feed is served from $output either way.
				global $wpdb;
				$wpdb->hide_errors();
				set_transient( $transient_key, $to_cache, 12 * HOUR_IN_SECONDS );
				$wpdb->show_errors();
			}

			echo $output; // phpcs:ignore WordPress.Security.EscapeOutput
		}

		exit;
	}

	/**
	 * Render the custom podcast RSS feed for a specific podcast post.
	 *
	 * @since  1.0.0
	 * @access private
	 * @param  WP_Post $podcast Podcast post object.
	 * @param  int
	 */
	private function render_feed( $podcast ) {
		$podcast_id  = (int) $podcast->ID;
		$podcast_url = get_permalink( $podcast_id );

		// Fetch episodes
		$args = array(
			'post_type'      => 'aiopod_episodes',
			'post_status'    => 'publish',
			'post_parent'    => $podcast_id,
			'posts_per_page' => apply_filters( 'aiopod_feed_episodes_limit', 300 ),
			'orderby'        => 'date',
			'order'          => 'DESC'
		);

		$episodes = get_posts( $args );

		// Proceed with RSS XML output...
		echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
		echo '<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">' . "\n";
		echo "\t" . '<channel>' . "\n";
		echo "\t\t" . '<title>' . $this->esc_xml( $podcast->post_title ) . '</title>' . "\n";

		$feed_url = aiopod_get_rss_feed_url( $podcast );
		echo "\t\t" . '<atom:link rel="self" href="' . esc_url( $feed_url ) . '" type="application/rss+xml" />' . "\n";

		echo "\t\t<lastBuildDate>" . mysql2date( 'r', $podcast->post_modified ) . "</lastBuildDate>\n";
		echo "\t\t<generator>" . $this->esc_xml( 'AiO Podcast Studio ' . AIOPOD_PLUGIN_VERSION ) . "</generator>\n";

		if ( ! empty( $podcast->post_excerpt ) ) {
			echo "\t\t<itunes:subtitle>" . $this->esc_xml( $podcast->post_excerpt ) . "</itunes:subtitle>\n";
		} elseif ( ! empty( $podcast->post_content ) ) {
			echo "\t\t<itunes:subtitle>" . $this->esc_xml( strip_tags( $podcast->post_content ) ) . "</itunes:subtitle>\n";
		}

		$new_feed_url = get_post_meta( $podcast_id, 'new_feed_url', true );
		if ( ! empty( $new_feed_url ) ) {
			echo "\t\t" . '<itunes:new-feed-url>' . esc_url( $new_feed_url ) . '</itunes:new-feed-url>' . "\n";
		}

		$website_url = get_post_meta( $podcast_id, 'website_url', true );
		if ( ! empty( $website_url ) ) {
			$website_url = str_replace( '%%podcast_url%%', $podcast_url, $website_url );
			echo "\t\t" . '<link>' . esc_url( $website_url ) . '</link>' . "\n";
		} else {
			echo "\t\t" . '<link>' . esc_url( $podcast_url ) . '</link>' . "\n";
		}

		$language = get_post_meta( $podcast_id, 'podcast_language', true );
		if ( empty( $language ) ) {
			if ( defined( 'ICL_SITEPRESS_VERSION' ) ) {
				// WPML: get the language of this specific post.
				$wpml_details = apply_filters( 'wpml_post_language_details', null, $podcast_id );
				if ( ! empty( $wpml_details['language_code'] ) ) {
					$language = strtolower( $wpml_details['language_code'] );
				}
			} elseif ( function_exists( 'pll_get_post_language' ) ) {
				// Polylang: get the language slug of this specific post.
				$pll_lang = pll_get_post_language( $podcast_id );
				if ( ! empty( $pll_lang ) ) {
					$language = strtolower( $pll_lang );
				}
			}

			if ( empty( $language ) ) {
				$locale_parts = explode( '_', get_locale() );
				$language     = strtolower( $locale_parts[0] );
			}
		}
		echo "\t\t" . '<language>' . $this->esc_xml( $language ) . '</language>' . "\n";

		$copyright = get_post_meta( $podcast_id, 'copyright_text', true );
		if ( ! empty( $copyright ) ) {
			echo "\t\t" . '<copyright>' . $this->esc_xml( $copyright ) . '</copyright>' . "\n";
		}

		$author_name  = get_post_meta( $podcast_id, 'author_name', true );
		$author_name  = str_replace( '%%author_name%%', get_the_author_meta( 'display_name', $podcast->post_author ), $author_name );

		$author_email = get_post_meta( $podcast_id, 'author_email', true );
		$author_email = str_replace( '%%author_email%%', get_the_author_meta( 'user_email', $podcast->post_author ), $author_email );

		if ( ! empty( $author_name ) || ! empty( $author_email ) ) {
			echo "\t\t" . '<itunes:owner>' . "\n";

			if ( ! empty( $author_name ) ) {
				echo "\t\t\t" . '<itunes:name>' . $this->esc_xml( $author_name ) . '</itunes:name>' . "\n";
			}

			if ( ! empty( $author_email ) ) {
				echo "\t\t\t" . '<itunes:email>' . $this->esc_xml( $author_email ) . '</itunes:email>' . "\n";
			}
			
			echo "\t\t" . '</itunes:owner>' . "\n";
		}

		if ( ! empty( $author_name ) ) {
			echo "\t\t" . '<itunes:author>' . $this->esc_xml( $author_name ) . '</itunes:author>' . "\n";
		}

		if ( ! empty( $podcast->post_content ) && ! empty( $podcast->post_excerpt ) ) {
			echo "\t\t" . '<description><![CDATA[' . wp_kses_post( $podcast->post_excerpt ) . ']]></description>' . "\n";
			echo "\t\t" . '<itunes:summary><![CDATA[' . wp_kses_post( $podcast->post_excerpt ) . ']]></itunes:summary>' . "\n";
			echo "\t\t" . '<content:encoded><![CDATA[' . wp_kses_post( $podcast->post_content ) . ']]></content:encoded>' . "\n";
		} else {
			if ( ! empty( $podcast->post_content ) ) {
				$post_excerpt = wp_strip_all_tags( $podcast->post_content );

				echo "\t\t" . '<description><![CDATA[' . wp_kses_post( $podcast->post_content ) . ']]></description>' . "\n";
				echo "\t\t" . '<itunes:summary><![CDATA[' . wp_kses_post( $post_excerpt ) . ']]></itunes:summary>' . "\n";
				echo "\t\t" . '<content:encoded><![CDATA[' . wp_kses_post( $podcast->post_content ) . ']]></content:encoded>' . "\n";
			}

			if ( ! empty( $podcast->post_excerpt ) ) {
				echo "\t\t" . '<description><![CDATA[' . wp_kses_post( $podcast->post_excerpt ) . ']]></description>' . "\n";
				echo "\t\t" . '<itunes:summary><![CDATA[' . wp_kses_post( $podcast->post_excerpt ) . ']]></itunes:summary>' . "\n";
			}
		}

		$podcast_type = get_post_meta( $podcast_id, 'podcast_type', true );
		if ( ! empty( $podcast_type ) ) {
			echo "\t\t" . '<itunes:type>' . $this->esc_xml( $podcast_type ) . '</itunes:type>' . "\n";
		}

		$image = aiopod_get_image( array( 'post' => $podcast, 'size' => 'full' ) );
		if ( ! empty( $image['src'] ) ) {
			echo "\t\t" . '<itunes:image href="' . esc_url( $image['src'] ) . '" />' . "\n";
		}

		$terms = wp_get_object_terms( $podcast_id, 'aiopod_categories' );
		
		if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
			$grouped_terms = array();

			foreach ( $terms as $term ) {
				if ( ! empty( $term->parent ) ) {
					$parent = get_term( $term->parent, 'aiopod_categories' );

					if ( is_wp_error( $parent ) || empty( $parent ) ) {
						// Fallback: treat as standalone
						$grouped_terms[0]['children'][] = $term;
						continue;
					}

					if ( ! isset( $grouped_terms[ $parent->term_id ] ) ) {
						$grouped_terms[ $parent->term_id ] = array(
							'parent'   => $parent,
							'children' => array()
						);
					}

					$grouped_terms[ $parent->term_id ]['children'][] = $term;
				} else {
					// Top-level category without children
					$grouped_terms[0]['children'][] = $term;
				}
			}

			// Output XML — Apple spec requires one parent block per subcategory.
			foreach ( $grouped_terms as $group ) {
				if ( ! empty( $group['parent'] ) && ! empty( $group['children'] ) ) {
					// One <itunes:category> wrapper per child, as required by Apple.
					foreach ( $group['children'] as $child ) {
						echo "\t\t" . '<itunes:category text="' . $this->esc_xml( $group['parent']->name ) . '">' . "\n";
						echo "\t\t\t" . '<itunes:category text="' . $this->esc_xml( $child->name ) . '" />' . "\n";
						echo "\t\t" . '</itunes:category>' . "\n";
					}
				} elseif ( ! empty( $group['children'] ) ) {
					// Standalone categories — skip any already output as a parent wrapper.
					foreach ( $group['children'] as $term ) {
						if ( ! isset( $grouped_terms[ $term->term_id ] ) ) {
							echo "\t\t" . '<itunes:category text="' . $this->esc_xml( $term->name ) . '" />' . "\n";
						}
					}
				}
			}
		}

		$is_explicit = (int) get_post_meta( $podcast_id, 'is_explicit', true );
		echo "\t\t" . '<itunes:explicit>' . ( $is_explicit ? 'true' : 'false' ) . '</itunes:explicit>' . "\n";

		$is_completed = (int) get_post_meta( $podcast_id, 'is_completed', true );
		if ( ! empty( $is_completed ) ) {
			echo "\t\t" . '<itunes:complete>Yes</itunes:complete>' . "\n";
		}

		$is_blocked = (int) get_post_meta( $podcast_id, 'is_blocked', true );
		if ( ! empty( $is_blocked ) ) {
			echo "\t\t" . '<itunes:block>Yes</itunes:block>' . "\n";
		}

		// Prime the episode tag term cache in a single query to avoid N+1
		// queries inside the loop below. wp_get_object_terms() checks this
		// cache before hitting the database, so every per-episode call will
		// be served from memory after this point.
		if ( ! empty( $episodes ) ) {
			$episode_ids_for_cache = wp_list_pluck( $episodes, 'ID' );
			wp_get_object_terms( $episode_ids_for_cache, 'aiopod_tags' );
		}

		foreach ( $episodes as $episode ) {
			$is_blocked = get_post_meta( $episode->ID, 'is_blocked', true );
			if ( $is_blocked ) continue;

			echo "\t\t" . '<item>' . "\n";

			echo "\t\t\t<link>" . esc_url( get_permalink( $episode->ID ) ) . "</link>\n";
			
			$episode_type = get_post_meta( $episode->ID, 'episode_type', true );
			if ( ! empty( $episode_type ) ) {
				echo "\t\t\t" . '<itunes:episodeType>' . $this->esc_xml( $episode_type ) . '</itunes:episodeType>' . "\n";
			}

			$episode_number = get_post_meta( $episode->ID, 'episode_number', true );
			if ( ! empty( $episode_number ) ) {
				echo "\t\t\t" . '<itunes:episode>' . intval( $episode_number ) . '</itunes:episode>' . "\n";
			}

			$season_number = get_post_meta( $episode->ID, 'season_number', true );
			if ( ! empty( $season_number ) ) {
				echo "\t\t\t" . '<itunes:season>' . intval( $season_number ) . '</itunes:season>' . "\n";
			}

			echo "\t\t\t" . '<title>' . $this->esc_xml( $episode->post_title ) . '</title>' . "\n";

			if ( ! empty( $episode->post_content ) ) {
				echo "\t\t\t" . '<description><![CDATA[' . wp_kses_post( $episode->post_content ) . ']]></description>' . "\n";
			} else {
				$description = get_post_meta( $episode->ID, 'description', true );
				if ( ! empty( $description ) ) {
					echo "\t\t\t" . '<description>' . $this->esc_xml( $description ) . '</description>' . "\n";
				}
			}

			$tags = wp_get_object_terms( $episode->ID, 'aiopod_tags', array( 'fields' => 'names' ) );
			if ( ! is_wp_error( $tags ) && ! empty( $tags ) ) {
				$keywords = implode( ', ', $tags );
				echo "\t\t\t" . '<itunes:keywords>' . $this->esc_xml( $keywords ) . '</itunes:keywords>' . "\n";
			}

			$image = aiopod_get_image( array( 'post' => $episode, 'size' => 'full' ) );
			if ( ! empty( $image['src'] ) ) {
				echo "\t\t\t" . '<itunes:image href="' . esc_url( $image['src'] ) . '" />' . "\n";
			}
			
			$episode_file       = get_post_meta( $episode->ID, 'episode_file', true );
			$episode_file_type  = aiopod_get_episode_file_type( $episode->ID, $episode_file );		
			$episode_file_size  = get_post_meta( $episode->ID, 'episode_file_size', true );
			$allowed_mime_types = apply_filters( 'aiopod_feed_allowed_mime_types', array( 'audio/mpeg', 'audio/mp4', 'video/mp4', 'video/x-m4v', 'video/quicktime' ) );

			// Enclosure requires at least URL + type
			if ( ! empty( $episode_file ) && ! empty( $episode_file_type ) && in_array( $episode_file_type, $allowed_mime_types, true ) ) {

				/**
				 * Whether to use the tracking URL for the RSS feed enclosure.
				 *
				 * When true (default), the enclosure URL is routed through the
				 * plugin's tracking endpoint so podcast-app downloads are counted.
				 * Set to false to use the raw episode file URL instead — useful
				 * when a third-party analytics proxy (e.g. Podtrac) is already
				 * in the feed, or when statistics are not needed.
				 *
				 * add_filter( 'aiopod_rss_tracking_enabled', '__return_false' );
				 *
				 * @since 1.0.0
				 * @param bool    $enabled  Whether tracking is enabled. Default true.
				 * @param WP_Post $episode  The episode post object.
				 */
				$rss_tracking_enabled = (bool) apply_filters( 'aiopod_rss_tracking_enabled', true, $episode );

				$enclosure_url = $rss_tracking_enabled ? aiopod_get_episode_file_url( $episode ) : $episode_file;

				$attributes = array(
					'url'  => esc_url( $enclosure_url ),
					'type' => esc_attr( $episode_file_type )
				);

				// Length is optional in the real world
				if ( ! empty( $episode_file_size ) && is_numeric( $episode_file_size ) ) {
					$attributes['length'] = esc_attr( (int) $episode_file_size );
				}

				$enclosure = '<enclosure';

				foreach ( $attributes as $key => $value ) {
					$enclosure .= sprintf( ' %s="%s"', $key, $value );
				}

				$enclosure .= ' />';

				echo "\t\t\t" . $enclosure . "\n";
			}

			$guid = get_post_meta( $episode->ID, 'guid', true );
			if ( empty( $guid ) ) {
				$guid = 'episode-' . $episode->ID;
			}

			echo sprintf(
				"\t\t\t<guid%s>%s</guid>\n",
				filter_var( $guid, FILTER_VALIDATE_URL ) ? '' : ' isPermaLink="false"',
				$this->esc_xml( $guid )
			);

			echo "\t\t\t" . '<pubDate>' . mysql2date( 'r', $episode->post_date ) . '</pubDate>' . "\n";

			$duration = get_post_meta( $episode->ID, 'episode_duration', true );
			if ( ! empty( $duration ) ) {
				echo "\t\t\t" . '<itunes:duration>' . $this->esc_xml( $duration ) . '</itunes:duration>' . "\n";
			}

			$episode_author = get_post_meta( $episode->ID, 'author_name', true );
			$episode_author = ! empty( $episode_author ) ? $episode_author : $author_name;
			if ( ! empty( $episode_author ) ) {
				echo "\t\t\t" . '<itunes:author>' . $this->esc_xml( $episode_author ) . '</itunes:author>' . "\n";
			}

			$is_explicit = (int) get_post_meta( $episode->ID, 'is_explicit', true );
			echo "\t\t\t" . '<itunes:explicit>' . ( $is_explicit ? 'true' : 'false' ) . '</itunes:explicit>' . "\n";

			$is_blocked = (int) get_post_meta( $episode->ID, 'is_blocked', true );
			if ( ! empty( $is_blocked ) ) {
				echo "\t\t\t" . '<itunes:block>Yes</itunes:block>' . "\n";
			}

			echo "\t\t" . '</item>' . "\n";
		}

		echo "\t" . '</channel>' . "\n";
		echo '</rss>';
	}

	/**
	 * Delete the RSS feed transient cache when a podcast or episode is saved.
	 *
	 * Hooked to save_post and before_delete_post so the cache is always
	 * consistent after any content change.
	 *
	 * @since  1.0.0
	 * @param  int     $post_id Post ID.
	 * @param  WP_Post $post    Post object.
	 */
	public function bust_feed_cache( $post_id, $post ) {
		if ( 'aiopod_podcasts' === $post->post_type ) {
			delete_transient( 'aiopod_feed_' . $post_id );
		} elseif ( 'aiopod_episodes' === $post->post_type && $post->post_parent > 0 ) {
			delete_transient( 'aiopod_feed_' . $post->post_parent );
		}
	}

	/**
	 * Escape and encode text for safe use in podcast RSS feeds.
	 *
	 * @since  1.0.0
	 * @param  string $content Raw content to be escaped.
     * @return string          Escaped and encoded content.
	 */
	public function esc_xml( $content ) {
		$special_chars = array(
			'©'      => '&#xA9;',   // Copyright sign
			'&copy;' => '&#xA9;',   // Copyright sign
			'℗'      => '&#x2117;', // Sound recording copyright
			'™'      => '&#x2122;', // Trademark
			'’'      => '&apos;',   // Apostrophe (curly)
			'“'      => '&quot;',   // Left quote
			'”'      => '&quot;',   // Right quote
		);

		// First, replace special characters
		$content = strtr( $content, $special_chars );

		// Then escape for XML output
		return esc_xml( $content );
	}	
	
}
