<?php

/**
 * The public-facing functionality of the plugin.
 *
 * @link    https://aiopodcasting.com
 * @since   1.0.0
 *
 * @package AiO_Podcast_Studio
 */

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

/**
 * AIOPOD_Public class.
 *
 * @since 1.0.0
 */
class AIOPOD_Public {
	
	/**
	 * Template redirect handler.
	 *
	 * - Fixes secondary loop pagination issues on single CPT pages.
	 * - Redirects default CPT archives to custom archive pages, if configured.
	 *
	 * @since 1.0.0
	 */
	public function template_redirect() {
		// Never run in admin or AJAX context
		if ( is_admin() || wp_doing_ajax() ) {
			return;
		}

		$this->fix_single_pagination();
		$this->redirect_custom_archives();
	}
	
	/**
	 * Register rewrite rules.
	 *
	 * @since 1.0.0
	 */
	public function init() {
		$pages_settings = aiopod_get_option( 'aiopod_pages_settings' );

		$site_url = home_url();
		
		// Rewrite tags
		add_rewrite_tag( '%aiopod_podcast%', '([^/]+)' );
		add_rewrite_tag( '%aiopod_episode%', '([^/]+)' );
		add_rewrite_tag( '%aiopod_category%', '([^/]+)' );
		add_rewrite_tag( '%aiopod_tag%', '([^/]+)' );
		add_rewrite_tag( '%aiopod_participant%', '([^/]+)' );
		add_rewrite_tag( '%aiopod_feed%', '([^/]+)' );
		add_rewrite_tag( '%aiopod_download%', '([^/]+)' );

		// RSS feed page
		$podcast_post_type = get_post_type_object( 'aiopod_podcasts' );

		if ( $podcast_post_type && ! empty( $podcast_post_type->rewrite['slug'] ) ) {
			$podcast_base = trim( $podcast_post_type->rewrite['slug'], '/' );
			add_rewrite_rule( "^{$podcast_base}/([^/]+)/feed/?$", 'index.php?aiopod_podcast=$matches[1]&aiopod_feed=podcast', 'top' );
		}

		// Episode download tracking — matches:
		//   /{base}/{slug}/file/{filename}.{ext}  (new format, with extension)
		//   /{base}/{slug}/file/                  (legacy format, no filename)
		$episode_post_type = get_post_type_object( 'aiopod_episodes' );

		if ( $episode_post_type && ! empty( $episode_post_type->rewrite['slug'] ) ) {
			$episode_base = trim( $episode_post_type->rewrite['slug'], '/' );
			add_rewrite_rule( "^{$episode_base}/([^/]+)/file(?:/[^/]+)?/?$", 'index.php?aiopod_episode=$matches[1]&aiopod_download=1', 'top' );
		}

		// Single category page		
		if ( ! empty( $pages_settings['category'] ) ) {
			$id   = (int) $pages_settings['category'];
			$post = get_post( $id );

			if ( $post && 'publish' === $post->post_status ) {
				$permalink = get_permalink( $id );

				if ( $permalink ) {
					$slug = str_replace( $site_url, '', $permalink );			
					$slug = trim( $slug, '/' );
					$slug = urldecode( $slug );		
					
					add_rewrite_rule( "^$slug/([^/]+)/page/?([0-9]{1,})/?$", 'index.php?page_id=' . $id . '&aiopod_category=$matches[1]&paged=$matches[2]', 'top' );
					add_rewrite_rule( "^$slug/([^/]+)/?$", 'index.php?page_id=' . $id . '&aiopod_category=$matches[1]', 'top' );
				}
			}
		}

		// Single tag page
		if ( ! empty( $pages_settings['tag'] ) ) {
			$id   = (int) $pages_settings['tag'];
			$post = get_post( $id );

			if ( $post && 'publish' === $post->post_status ) {
				$permalink = get_permalink( $id );

				if ( $permalink ) {
					$slug = str_replace( $site_url, '', $permalink );			
					$slug = trim( $slug, '/' );
					$slug = urldecode( $slug );		
					
					add_rewrite_rule( "^$slug/([^/]+)/page/?([0-9]{1,})/?$", 'index.php?page_id=' . $id . '&aiopod_tag=$matches[1]&paged=$matches[2]', 'top' );
					add_rewrite_rule( "^$slug/([^/]+)/?$", 'index.php?page_id=' . $id . '&aiopod_tag=$matches[1]', 'top' );
				}
			}
		}

		// Single participant page
		if ( ! empty( $pages_settings['participant'] ) ) {
			$id   = (int) $pages_settings['participant'];
			$post = get_post( $id );

			if ( $post && 'publish' === $post->post_status ) {
				$permalink = get_permalink( $id );

				if ( $permalink ) {
					$slug = str_replace( $site_url, '', $permalink );			
					$slug = trim( $slug, '/' );
					$slug = urldecode( $slug );		
					
					add_rewrite_rule( "^$slug/([^/]+)/page/?([0-9]{1,})/?$", 'index.php?page_id=' . $id . '&aiopod_participant=$matches[1]&paged=$matches[2]', 'top' );
					add_rewrite_rule( "^$slug/([^/]+)/?$", 'index.php?page_id=' . $id . '&aiopod_participant=$matches[1]', 'top' );
				}
			}
		}

		// Podcast archive page
		if ( ! empty( $pages_settings['podcast_archive_page'] ) ) {
			$id   = (int) $pages_settings['podcast_archive_page'];
			$post = get_post( $id );

			if ( $post && 'publish' === $post->post_status ) {
				$permalink = get_permalink( $id );

				if ( $permalink ) {
					$slug = str_replace( $site_url, '', $permalink );			
					$slug = trim( $slug, '/' );
					$slug = urldecode( $slug );		
					
					add_rewrite_rule( "^$slug/([^/]+)/page/?([0-9]{1,})/?$", 'index.php?post_type=aiopod_podcasts&name=$matches[1]&paged=$matches[2]', 'top' );
					add_rewrite_rule( "^$slug/([^/]+)/?$", 'index.php?post_type=aiopod_podcasts&name=$matches[1]', 'top' );
					
					add_rewrite_rule( "^$slug/page/?([0-9]{1,})/?$", 'index.php?page_id=' . $id . '&paged=$matches[1]', 'top' );
				}
			}
		}

		// Episode archive page
		if ( ! empty( $pages_settings['episode_archive_page'] ) ) {
			$id   = (int) $pages_settings['episode_archive_page'];
			$post = get_post( $id );

			if ( $post && 'publish' === $post->post_status ) {
				$permalink = get_permalink( $id );

				if ( $permalink ) {
					$slug = str_replace( $site_url, '', $permalink );			
					$slug = trim( $slug, '/' );
					$slug = urldecode( $slug );		
					
					add_rewrite_rule( "^$slug/([^/]+)/page/?([0-9]{1,})/?$", 'index.php?post_type=aiopod_episodes&name=$matches[1]&paged=$matches[2]', 'top' );
					add_rewrite_rule( "^$slug/([^/]+)/?$", 'index.php?post_type=aiopod_episodes&name=$matches[1]', 'top' );
					
					add_rewrite_rule( "^$slug/page/?([0-9]{1,})/?$", 'index.php?page_id=' . $id . '&paged=$matches[1]', 'top' );
				}
			}
		}
	}
	
	/**
	 * Register custom query vars.
	 *
	 * @since 1.0.0
	 */
	public function query_vars( $vars ) {
		$vars[] = 'view';
		$vars[] = 'aiopod_podcast';
		$vars[] = 'aiopod_episode';
		$vars[] = 'aiopod_category';
		$vars[] = 'aiopod_tag';
		$vars[] = 'aiopod_participant';
		$vars[] = 'aiopod_feed';
		$vars[] = 'aiopod_download';

		return $vars;
	}

	/**
	 * Resolve pages that sit underneath a custom archive page.
	 *
	 * The archive rules map `{archive-page}/{segment}` to a single podcast or episode. A child page
	 * of that same archive page has the identical shape, and because these rules are registered at
	 * the top of the list they are consulted before WordPress reaches its own page rules — so the
	 * page is looked up as a podcast, is not found, and returns 404.
	 *
	 * This reaches any page nested under an archive page: the category, tag and participant pages
	 * when they are children of it, and the episode archive page itself when it is a child of the
	 * podcast one.
	 *
	 * An existing podcast or episode always keeps the address, so the requests these rules were
	 * written to serve are untouched. Only a slug matching no post of that type, but matching a
	 * published page in the archive page's own branch, is handed back to WordPress as a page.
	 *
	 * @since  1.0.4
	 * @param  array $query_vars Query variables built from the matched rewrite rule.
	 * @return array $query_vars Query variables for this request to use.
	 */
	public function resolve_archive_child_pages( $query_vars ) {
		if ( empty( $query_vars['name'] ) || empty( $query_vars['post_type'] ) ) {
			return $query_vars;
		}

		$archive_pages = array(
			'aiopod_podcasts' => 'podcast_archive_page',
			'aiopod_episodes' => 'episode_archive_page'
		);

		if ( ! isset( $archive_pages[ $query_vars['post_type'] ] ) ) {
			return $query_vars;
		}

		$pages_settings = aiopod_get_option( 'aiopod_pages_settings' );
		$setting        = $archive_pages[ $query_vars['post_type'] ];

		if ( empty( $pages_settings[ $setting ] ) ) {
			return $query_vars;
		}

		$archive_uri = get_page_uri( (int) $pages_settings[ $setting ] );

		if ( empty( $archive_uri ) ) {
			return $query_vars;
		}

		// Tested first because it fails for every ordinary podcast and episode, which is the
		// overwhelming majority of what reaches here, and settles them in a single lookup
		$page = get_page_by_path( trailingslashit( $archive_uri ) . $query_vars['name'] );

		if ( empty( $page ) || 'publish' !== $page->post_status ) {
			return $query_vars;
		}

		// Whatever its status, a post of this type owns the address, so a page only takes it over
		// where no such post exists
		if ( get_page_by_path( $query_vars['name'], OBJECT, $query_vars['post_type'] ) ) {
			return $query_vars;
		}

		$resolved = array( 'page_id' => $page->ID );

		// Carried over so a paginated child page still lands on the right page of its own loop
		if ( ! empty( $query_vars['paged'] ) ) {
			$resolved['paged'] = $query_vars['paged'];
		}

		// `WP_Query::parse_query()` sets `is_preview` from this var alone, and `the_preview` — the
		// filter that swaps the revision in — runs only when it is set. Dropping it would show the
		// published page instead of the unsaved changes when a nested page is previewed
		if ( ! empty( $query_vars['preview'] ) ) {
			$resolved['preview'] = $query_vars['preview'];
		}

		return $resolved;
	}

	/**
	 * Add podcasts and episodes to the core category and tag archives.
	 *
	 * `/category/foo/` and `/tag/bar/` set no `post_type` of their own, so `WP_Query` falls back
	 * to `post` alone. Without this, a core term link on a podcast leads to an archive that does
	 * not contain the podcast the visitor came from — it shows the wrong posts, or nothing.
	 *
	 * Those archives stay the theme's: our posts are rendered by `archive.php` alongside blog
	 * posts, with no player. That is the intended result of the setting, and the reason the whole
	 * feature is off by default.
	 *
	 * Registered at priority 99 and merges rather than replaces, so a plugin setting `post_type`
	 * at the usual priority 10 cannot drop us. `aiopod_core_archive_post_types` opts back out.
	 *
	 * @since 1.0.4
	 * @param WP_Query $query The query object, before it runs.
	 */
	public function include_in_core_archives( $query ) {
		// Never the admin list tables, and never a theme's or a plugin's secondary loop
		if ( is_admin() || ! $query->is_main_query() ) {
			return;
		}

		// Deliberately not guarded against `is_feed()`. `/category/foo/feed/` is the core feed of
		// a core archive: if the archive lists our episodes, its feed lists them too. This does
		// not touch the plugin's own podcast feeds
		if ( ! $query->is_category() && ! $query->is_tag() ) {
			return;
		}

		$taxonomies_settings = aiopod_get_option( 'aiopod_taxonomies_settings' );

		if ( empty( $taxonomies_settings['wp_taxonomies'] ) || empty( $taxonomies_settings['include_in_core_archives'] ) ) {
			return;
		}

		// The archive being viewed has to belong to a taxonomy that is actually enabled. A site
		// running tags only must find its category archives exactly as they were
		$setting = $query->is_category() ? 'categories' : 'tags';

		if ( ! isset( $taxonomies_settings['wp_taxonomies'][ $setting ] ) ) {
			return;
		}

		$existing = $query->get( 'post_type' );

		// `any` is a keyword, not a list, and already covers both post types
		if ( 'any' === $existing ) {
			return;
		}

		// Additive, never a replacement - another plugin's post type stays. An empty value is
		// the `post` that WP_Query would have fallen back to on its own
		$post_types = empty( $existing ) ? array( 'post' ) : (array) $existing;
		$post_types = array_unique( array_merge( $post_types, array( 'aiopod_podcasts', 'aiopod_episodes' ) ) );

		$query->set( 'post_type', apply_filters( 'aiopod_core_archive_post_types', $post_types, $query ) );
	}

	/**
	 * Register the stylesheets for the public-facing side of the site.
	 *
	 * @since 1.0.0
	 */
	public function register_styles() {
		$performance_settings = aiopod_get_option( 'aiopod_performance_settings' );
		$appearance_settings  = aiopod_get_option( 'aiopod_appearance_settings' );

		wp_register_style(
			AIOPOD_PLUGIN_SLUG . '-public',
			AIOPOD_PLUGIN_URL . 'public/assets/css/public.min.css',
			array(),
			AIOPOD_PLUGIN_VERSION,
			'all'
		);

		$primary_color = ! empty( $appearance_settings['primary_color'] ) ? esc_attr( $appearance_settings['primary_color'] ) : '#00b2ff';
		$inline_css    = '.aiopod { --aiopod-color-main: ' . $primary_color . '; --plyr-color-main: ' . $primary_color . '; --swiper-theme-color: ' . $primary_color . '; }';

		wp_add_inline_style( AIOPOD_PLUGIN_SLUG . '-public', $inline_css );

		// Force load the plugin's CSS file
		if ( isset( $performance_settings['force_load_assets']['css'] ) ) {
			wp_enqueue_style( AIOPOD_PLUGIN_SLUG . '-public' );
		}
	}

	/**
	 * Register the JavaScript for the public-facing side of the site.
	 *
	 * @since 1.0.0
	 */
	public function register_scripts() {
		$performance_settings = aiopod_get_option( 'aiopod_performance_settings' );
		$episodes_settings    = aiopod_get_option( 'aiopod_episodes_settings' );

		$scroll_offset = ! empty( $episodes_settings['scroll_offset'] ) ? (int) $episodes_settings['scroll_offset'] : 0;
		if ( $scroll_offset && is_admin_bar_showing() ) $scroll_offset += 32;
		$scroll_offset = apply_filters( 'aiopod_scroll_offset', $scroll_offset );

		// Register the dependencies
		wp_register_script(
			AIOPOD_PLUGIN_SLUG . '-public',
			AIOPOD_PLUGIN_URL . 'public/assets/js/public.min.js',
			array( 'jquery' ),
			AIOPOD_PLUGIN_VERSION,
			array( 'strategy' => 'defer' )
		);			

		wp_localize_script(
			AIOPOD_PLUGIN_SLUG . '-public',
			'aiopod',
			array(
				'plugin_url'     => AIOPOD_PLUGIN_URL,
				'ajax_url'       => admin_url( 'admin-ajax.php' ),
				'ajax_nonce'     => wp_create_nonce( 'aiopod_ajax_nonce' ),
				'scroll_offset'  => $scroll_offset,
				'can_play_video' => apply_filters( 'aiopod_player_can_play_video', false ),
			)
		);	
		
		// Force load the plugin's JavaScript file
		if ( isset( $performance_settings['force_load_assets']['js'] ) ) {
			wp_enqueue_script( AIOPOD_PLUGIN_SLUG . '-public' );
		}
	}	

	/**
	 * Set MySQL's RAND function seed value in a cookie.
	 *
	 * @since 1.0.0
	 */
	public function set_mysql_rand_seed_value() {
		$privacy_settings = aiopod_get_option( 'aiopod_privacy_settings' );
		
		if ( isset( $privacy_settings['disable_cookies'] ) && isset( $privacy_settings['disable_cookies']['aiopod_rand_seed'] ) ) {
			unset( $_COOKIE['aiopod_rand_seed'] );
			return false;
		}
		
		if ( headers_sent() ) {
			return false;
		}
		
		$paged = aiopod_get_page_number();
		if ( ! isset( $_COOKIE['aiopod_rand_seed'] ) || 1 === $paged ) {
			$seed = wp_rand();
			setcookie( 'aiopod_rand_seed', $seed, time() + ( 86400 * 1 ), COOKIEPATH, COOKIE_DOMAIN );

			// Update $_COOKIE for immediate use in this request
			$_COOKIE['aiopod_rand_seed'] = $seed;
		}
	}

	/**
	 * Enqueue block assets inside the block editor (iframe).
	 *
	 * Hooked to enqueue_block_assets with an is_admin() guard so styles and scripts
	 * are injected inside the iframed block editor (WP 6.3+ / WP 7.0 always) only,
	 * and not duplicated on the front end where wp_enqueue_scripts already handles them.
	 *
	 * @since 1.0.0
	 */
	public function enqueue_block_assets() {
		if ( ! is_admin() ) {
			return;
		}

		$this->enqueue_editor_assets();
	}

	/**
	 * Enqueue the plugin's public styles and scripts in any editor context.
	 *
	 * Called by enqueue_block_assets() (WordPress block editor, guarded by is_admin())
	 * and hooked directly to Elementor actions so assets are also available in the
	 * Elementor editor panel and its frontend live-preview iframe:
	 *   - elementor/editor/after_enqueue_scripts  (admin context)
	 *   - elementor/preview/enqueue_scripts       (frontend context, is_admin() = false)
	 *
	 * @since 1.0.0
	 */
	public function enqueue_editor_assets() {
		// Styles
		$this->register_styles();

		wp_enqueue_style( AIOPOD_PLUGIN_SLUG . '-public' );

		// Scripts
		$this->register_scripts();
		
		wp_enqueue_script( AIOPOD_PLUGIN_SLUG . '-public' );
	}

	/**
	 * Flush rewrite rules when it's necessary.
	 *
	 * @since 1.0.0
	 */
	 public function maybe_flush_rules() {
		// One-time delayed flush after activation/update/settings change.
		// Runs on wp_loaded (after init), so all CPTs, taxonomies and rewrite
		// rules are registered. Use a hard flush here so the server-level
		// permalink config (e.g. .htaccess) is rebuilt too — a soft flush only
		// updates the rewrite_rules option and can leave feed/CPT URLs 404ing
		// on a fresh install until permalinks are saved manually.
		if ( ! get_option( 'aiopod_rewrite_rules_flushed' ) ) {
			flush_rewrite_rules( true );
			update_option( 'aiopod_rewrite_rules_flushed', 1 );
			
			return;
		}

		$performance_settings = aiopod_get_option( 'aiopod_performance_settings' );

		if ( empty( $performance_settings['maybe_flush_rewrite_rules'] ) ) {
			return;
		}

		// Allow developers to restrict the check to admin only
		$admin_only = apply_filters( 'aiopod_flush_rewrite_rules_admin_only', false );

		if ( $admin_only && ! is_admin() ) {
			return;
		}

		$stored_rules = get_option( 'rewrite_rules' );

		if ( empty( $stored_rules ) || ! is_array( $stored_rules ) ) {
			return;
		}

		global $wp_rewrite;

		if ( empty( $wp_rewrite ) ) {
			return;
		}

		$generated_rules = $wp_rewrite->rewrite_rules();

		if ( empty( $generated_rules ) || ! is_array( $generated_rules ) ) {
			return;
		}

		foreach ( $generated_rules as $rule => $rewrite ) {
			if ( ! array_key_exists( $rule, $stored_rules ) ) {
				flush_rewrite_rules( false );
				return;
			}
		}
	}	
	
	/**		 
	 * Override the default page/post title.
	 *		
	 * @since  1.0.0
	 * @param  string $title       The document title.	 
     * @param  string $sep         Title separator.
     * @param  string $seplocation Location of the separator (left or right).		 
	 * @return string              The filtered title.		 
	*/
	public function wp_title( $title, $sep, $seplocation ) {		
		global $post;
		
		if ( ! isset( $post ) ) return $title;
		
		$pages_settings = aiopod_get_option( 'aiopod_pages_settings' );
		$site_name      = sanitize_text_field( get_bloginfo( 'name' ) );
		$custom_title   = '';		
		
		// Get category page title
		if ( $post->ID === (int) $pages_settings['category'] ) {
			if ( $slug = get_query_var( 'aiopod_category' ) ) {
				if ( $term = get_term_by( 'slug', $slug, 'aiopod_categories' ) ) {
					$custom_title = $term->name;
				}
			}
		}

		// Get tag page title
		if ( $post->ID === (int) $pages_settings['tag'] ) {
			if ( $slug = get_query_var( 'aiopod_tag' ) ) {
				if ( $term = get_term_by( 'slug', $slug, 'aiopod_tags' ) ) {
					$custom_title = $term->name;
				}
			}
		}

		// Get participant page title
		if ( $post->ID === (int) $pages_settings['participant'] ) {
			if ( $slug = get_query_var( 'aiopod_participant' ) ) {
				if ( $term = get_term_by( 'slug', $slug, 'aiopod_participants' ) ) {
					$custom_title = $term->name;
				}
			}
		}

		// Set custom title
		if ( ! empty( $custom_title ) ) {
			$title = ( 'left' === $seplocation ) ? "$site_name $sep $custom_title" : "$custom_title $sep $site_name";
		}
		
		return $title;		
	}
	
	/**
	 * Override the default post/page title depending on the AIOPOD view.
	 *
	 * @since  1.0.0
	 * @param  array $title The document title parts.
	 * @return              Filtered title parts.
	 */
	public function document_title_parts( $title ) {	
		global $post;
		
		if ( ! isset( $post ) ) return $title;
		
		$pages_settings = aiopod_get_option( 'aiopod_pages_settings' );
		
		// Get category page title
		if ( $post->ID === (int) $pages_settings['category'] ) {
			if ( $slug = get_query_var( 'aiopod_category' ) ) {
				$term = get_term_by( 'slug', $slug, 'aiopod_categories' );
				$title['title'] = $term->name;
			}
		}

		// Get tag page title
		if ( $post->ID === (int) $pages_settings['tag'] ) {
			if ( $slug = get_query_var( 'aiopod_tag' ) ) {
				if ( $term = get_term_by( 'slug', $slug, 'aiopod_tags' ) ) {
					$title['title'] = $term->name;
				}
			}
		}

		// Get participant page title
		if ( $post->ID === (int) $pages_settings['participant'] ) {
			if ( $slug = get_query_var( 'aiopod_participant' ) ) {
				if ( $term = get_term_by( 'slug', $slug, 'aiopod_participants' ) ) {
					$title['title'] = $term->name;
				}
			}
		}
		
		// Return
		return $title;	
	}

	/**
	 * Adds Facebook Open Graph tags and Twitter Cards to the page head.
	 *
	 * @since 1.0.0
	 */
	public function wp_head() {
		$seo_settings = aiopod_get_option( 'aiopod_seo_settings' );

		// Facebook Open Graph tags and Twitter Cards.
		global $post;		

		$content_type = 'page';
		$term_slug    = '';

		if ( is_singular( 'aiopod_podcasts' ) ) {
			$content_type = 'podcast';
		} elseif ( is_singular( 'aiopod_episodes' ) ) {
			$content_type = 'episode';
		} elseif ( $term_slug = get_query_var( 'aiopod_category' ) ) {
			$content_type = 'category';
		} elseif ( $term_slug = get_query_var( 'aiopod_tag' ) ) {
			$content_type = 'tag';
		} elseif ( $term_slug = get_query_var( 'aiopod_participant' ) ) {
			$content_type = 'participant';
		}

		// Check if OG tags are enabled for this content type.
		if ( empty( $seo_settings['open_graph_tags'] ) || ! isset( $seo_settings['open_graph_tags'][ $content_type ] ) ) {
			return;
		}

		$site_name        = get_bloginfo( 'name' );
		$twitter_username = ! empty( $seo_settings['twitter_username'] ) ? trim( $seo_settings['twitter_username'] ) : '';
		$page_url         = '';
		$page_title       = '';
		$page_description = '';
		$image_url        = '';
		$media_url        = '';
		$media_type       = '';
		$is_video         = 0;

		/**
		 * Category / Tag / Participant pages (custom query vars)
		 */
		if ( in_array( $content_type, array( 'category', 'tag', 'participant' ) ) ) {
			$taxonomy = 'aiopod_categories';
			if ( 'tag' === $content_type ) {
				$taxonomy = 'aiopod_tags';
			} elseif ( 'participant' === $content_type ) {
				$taxonomy = 'aiopod_participants';
			}

			$term     = get_term_by( 'slug', $term_slug, $taxonomy );

			if ( ! $term || is_wp_error( $term ) ) {
				return;
			}

			$page_url         = get_term_link( $term );
			$page_title       = trim( $term->name );			
			$page_description = aiopod_truncate_text( $term->description, 160, '' );

			$image_data = aiopod_get_image(	array(
				'term_id' => $term->term_id,
				'size'    => 'large',
				'alt'     => ''
			) );

			$image_url = ! empty( $image_data['src'] ) ? $image_data['src'] : '';

		/**
		 * Singular post pages
		 */
		} else {
			if ( empty( $post ) || ! isset( $post->ID ) ) {
				return;
			}

			$page_url         = get_permalink( $post );
			$page_title       = trim( get_the_title( $post ) );
			$page_description = aiopod_get_excerpt( $post->ID, 160, '', false );

			$image_data = aiopod_get_image(	array(
				'post_id' => $post->ID,
				'size'    => 'large',
				'alt'     => ''
			) );

			$image_url = ! empty( $image_data['src'] ) ? $image_data['src'] : '';

			$media_url = get_post_meta( $post->ID, 'episode_file', true );
			if ( ! empty( $media_url ) ) {
				$media_type = aiopod_get_episode_file_type( $post->ID, $media_url );
				$is_video   = false !== strpos( $media_type, 'audio/' );
			}			
		}

		/**
		 * Open Graph type mapping
		 */
		$og_type = 'website';

		switch ( $content_type ) {
			case 'podcast':
				$og_type = 'music.album';
				break;

			case 'episode':
				$og_type = $is_video ? 'video.episode' : 'music.song';
				break;

			case 'participant':
				$og_type = 'profile';
				break;
		}

		// Fallback title
		if ( ! $page_title ) {
			$page_title = $site_name;
		}

		/**
		 * Output tags
		 */
		echo PHP_EOL . '<!-- AiO Podcast Studio: Open Graph / Twitter Cards -->' . PHP_EOL;
		echo sprintf( '<meta property="og:site_name" content="%s" />' . PHP_EOL, esc_attr( $site_name ) );
		echo sprintf( '<meta property="og:url" content="%s" />' . PHP_EOL, esc_url( $page_url ) );
		echo sprintf( '<meta property="og:type" content="%s" />' . PHP_EOL, esc_attr( $og_type ) );
		echo sprintf( '<meta property="og:title" content="%s" />' . PHP_EOL, esc_attr( $page_title ) );

		if ( $page_description ) {
			echo sprintf( '<meta property="og:description" content="%s" />' . PHP_EOL, esc_attr( $page_description ) );
		}

		if ( $image_url ) {
			echo sprintf( '<meta property="og:image" content="%s" />' . PHP_EOL, esc_url( $image_url ) );
		}

		/**
		 * Media tags (Open Graph)
		 */
		if ( $media_url ) {
			if ( $is_video ) {
				echo sprintf( '<meta property="og:video" content="%s" />' . PHP_EOL, esc_url( $media_url ) );

				if ( $media_type ) {
					echo sprintf( '<meta property="og:video:type" content="%s" />' . PHP_EOL, esc_attr( $media_type ) );
				}

				if ( 0 === strpos( $media_url, 'https://' ) ) {
					echo sprintf( '<meta property="og:video:secure_url" content="%s" />' . PHP_EOL, esc_url( $media_url ) );
				}

				echo '<meta property="og:video:width" content="1280" />' . PHP_EOL;
				echo '<meta property="og:video:height" content="720" />' . PHP_EOL;
			} else {
				echo sprintf( '<meta property="og:audio" content="%s" />' . PHP_EOL, esc_url( $media_url ) );

				if ( $media_type ) {
					echo sprintf( '<meta property="og:audio:type" content="%s" />' . PHP_EOL, esc_attr( $media_type ) );
				}

				if ( 0 === strpos( $media_url, 'https://' ) ) {
					echo sprintf( '<meta property="og:audio:secure_url" content="%s" />' . PHP_EOL, esc_url( $media_url ) );
				}
			}
		}

		/**
		 * Twitter Cards (summary only – no player yet)
		 */
		echo '<meta name="twitter:card" content="summary_large_image" />' . PHP_EOL;

		if ( $twitter_username ) {
			if ( '@' !== $twitter_username[0] ) $twitter_username = '@' . $twitter_username;
			echo sprintf( '<meta name="twitter:site" content="%s" />' . PHP_EOL, esc_attr( $twitter_username ) );
		}

		echo sprintf( '<meta name="twitter:title" content="%s" />' . PHP_EOL, esc_attr( $page_title ) );

		if ( $page_description ) {
			echo sprintf( '<meta name="twitter:description" content="%s" />' . PHP_EOL, esc_attr( $page_description ) );
		}

		if ( $image_url ) {
			echo sprintf( '<meta name="twitter:image" content="%s" />' . PHP_EOL, esc_url( $image_url ) );
		}
	}

	/**
	 * Change the current page title if applicable.
	 *
	 * @since  1.0.0
	 * @param  string $title   Current page title.
	 * @param  int    $post_id The post ID.
	 * @return string $title   Filtered page title.
	 */
	public function the_title( $title, $id = 0 ) {
		if ( ! in_the_loop() || ! is_main_query() ) {
			return $title;
		}

		$post_id = get_the_ID();

		if ( ! empty( $id ) ) {
			if ( $post_id !== $id ) {
				return $title;
			}
		}

		$pages_settings = aiopod_get_option( 'aiopod_pages_settings' );

		// Change category page title
		if ( $post_id === (int) $pages_settings['category'] ) {
			if ( $slug = get_query_var( 'aiopod_category' ) ) {
				if ( $term = get_term_by( 'slug', $slug, 'aiopod_categories' ) ) {
					$title = $term->name;
				}
			}
		}

		// Change tag page title
		if ( $post_id === (int) $pages_settings['tag'] ) {
			if ( $slug = get_query_var( 'aiopod_tag' ) ) {
				if ( $term = get_term_by( 'slug', $slug, 'aiopod_tags' ) ) {
					$title = $term->name;
				}
			}
		}

		// Change participant page title
		if ( $post_id === (int) $pages_settings['participant'] ) {
			if ( $slug = get_query_var( 'aiopod_participant' ) ) {
				if ( $term = get_term_by( 'slug', $slug, 'aiopod_participants' ) ) {
					$title = $term->name;
				}
			}
		}
		
		return $title;	
	}

	/**
	 * Filters whether a podcast-related post has a thumbnail.
	 *
	 * This ensures that AiO Podcast Studio posts can report a thumbnail
	 * even when the image is provided via plugin-specific image settings,
	 * while still respecting user options to hide featured images.
	 *
	 * @since  1.0.0
	 * @param  bool             $has_thumbnail Whether the post has a thumbnail.
	 * @param  int|WP_Post|null $post          Post ID or WP_Post object.
	 * @param  int|string       $thumbnail_id  Post thumbnail ID or empty string.
	 * @return bool             Whether the post should be treated as having a thumbnail.
	 */
	public function has_post_thumbnail( $has_thumbnail, $post, $thumbnail_id ) {
		$post = get_post( $post );

		// Bail early if the post object is invalid.
		if ( ! $post ) {
			return $has_thumbnail;
		}

		// Apply only on single podcast-related post types.
		if ( ! is_singular( array( 'aiopod_podcasts', 'aiopod_episodes' ) ) ) {
			return $has_thumbnail;
		}

		// Ensure this runs only for the main queried object.
		global $wp_the_query;

		if ( (int) $post->ID !== (int) $wp_the_query->get_queried_object_id() ) {
			return $has_thumbnail;
		}

		// Respect plugin image visibility settings.
		$appearance_settings = aiopod_get_option( 'aiopod_appearance_settings' );

		if ( ! empty( $appearance_settings['hide_on_single_post_pages'] ) ) {
			return false;
		}

		// If a native featured image exists, honor WordPress behavior.
		if ( ! empty( $thumbnail_id ) ) {
			return $has_thumbnail;
		}

		// Fallback: Check for plugin-managed images.
		$image_data = aiopod_get_image(	array(
			'post_id' => $post->ID,
			'size'    => 'large',
			'alt'     => ''
		) );

		if ( ! empty( $image_data['src'] ) ) {
			return true;
		}

		return $has_thumbnail;
	}

	/**
	 * Filters the podcast post thumbnail HTML.
	 *
	 * Allows AiO Podcast Studio posts to render thumbnails provided
	 * via plugin-specific image settings when a native featured image
	 * is not available, while still respecting user visibility options.
	 *
	 * @since  1.0.0
	 * @param  string       $html              The post thumbnail HTML.
	 * @param  int          $post_id           The post ID.
	 * @param  int|string   $post_thumbnail_id The post thumbnail ID or empty string.
	 * @param  string|array $size              The post thumbnail size.
	 * @param  array        $attr              Image attributes.
	 * @return string       The filtered post thumbnail HTML.
	 */
	public function post_thumbnail_html( $html, $post_id, $post_thumbnail_id, $size, $attr ) {
		// Apply only on single podcast-related post types.
		if ( ! is_singular( array( 'aiopod_podcasts', 'aiopod_episodes' ) ) ) {
			return $html;
		}

		// Ensure this runs only for the main queried object.
		global $wp_the_query;

		if ( (int) $post_id !== (int) $wp_the_query->get_queried_object_id() ) {
			return $html;
		}

		// Respect plugin image visibility settings.
		$appearance_settings = aiopod_get_option( 'aiopod_appearance_settings' );

		if ( ! empty( $appearance_settings['hide_on_single_post_pages'] ) ) {
			return '';
		}

		// If a native featured image exists, honor WordPress behavior.
		if ( ! empty( $post_thumbnail_id ) ) {
			return $html;
		}

		$thumbnail_html = '';

		/**
		 * 1. Plugin-managed attachment image (preferred)
		 */
		$image_id = get_post_meta( $post_id, 'image_id', true );

		if ( $image_id ) {
			$thumbnail_html = wp_get_attachment_image( $image_id, $size, false, $attr );
		}

		/**
		 * 2. External image URL fallback
		 */
		if ( ! $thumbnail_html ) {
			$image_url = get_post_meta( $post_id, 'image', true );

			if ( $image_url ) {
				$attributes = wp_parse_args( (array) $attr, array( 'alt' => get_the_title( $post_id ) )	);

				/**
				 * Match WordPress image attribute filtering behavior.
				 */
				$attributes = apply_filters( 'wp_get_attachment_image_attributes', $attributes, null, $size	);
				$attributes = array_map( 'esc_attr', $attributes );

				$thumbnail_html = sprintf( '<img src="%s"', esc_url( $image_url ) );

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

				$thumbnail_html .= ' />';
			}
		}

		return $thumbnail_html ? $thumbnail_html : $html;
	}

	/**
	 * Always use our custom page for AIOPOD categories & tags.
	 *
	 * @since  1.0.0
	 * @param  string $url      The term URL.
	 * @param  object $term     The term object.
	 * @param  string $taxonomy The taxonomy slug.
	 * @return string $url      Filtered term URL.
	 */
	public function term_link( $url, $term, $taxonomy ) {	
		if ( 'aiopod_categories' === $taxonomy ) {
			$url = aiopod_get_category_page_url( $term );
		}

		if ( 'aiopod_tags' === $taxonomy ) {
			$url = aiopod_get_tag_page_url( $term );
		}

		if ( 'aiopod_participants' === $taxonomy ) {
			$url = aiopod_get_participant_page_url( $term );
		}
		
		return $url;		
	}
	
	/**
	 * Fix pagination on single podcast / episode / participant pages.
	 *
	 * @access private
	 * @since  1.0.0
	 */
	private function fix_single_pagination() {
		if ( ! is_singular( array( 'aiopod_podcasts', 'aiopod_episodes' ) ) ) {
			return;
		}

		global $wp_query;

		if ( ! $wp_query->is_main_query() ) {
			return;
		}

		$page = (int) $wp_query->get( 'page' );

		if ( $page > 1 ) {
			// Convert `page` to `paged`
			$wp_query->set( 'page', 1 );
			$wp_query->set( 'paged', $page );
		}

		// Prevent canonical redirect breaking pagination
		remove_action( 'template_redirect', 'redirect_canonical' );
	}

	/**
	 * Redirect default CPT archives to custom archive pages.
	 *
	 * @access private
	 * @since  1.0.0
	 */
	private function redirect_custom_archives() {
		if ( ! is_post_type_archive() ) {
			return;
		}

		$pages_settings = aiopod_get_option( 'aiopod_pages_settings' );

		// Explicit CPT → option key mapping (future-proof)
		$archive_map = array(
			'aiopod_podcasts' => 'podcast_archive_page',
			'aiopod_episodes' => 'episode_archive_page'
		);

		$post_type = get_query_var( 'post_type' );

		if ( empty( $archive_map[ $post_type ] ) ) {
			return;
		}

		$option_key = $archive_map[ $post_type ];

		if ( empty( $pages_settings[ $option_key ] ) ) {
			return;
		}

		$page_id  = (int) $pages_settings[ $option_key ];
		$page_url = get_permalink( $page_id );

		if ( ! $page_url ) {
			return;
		}

		$current_url = home_url( add_query_arg( null, null ) );

		// Avoid redirect loop
		if ( untrailingslashit( $current_url ) === untrailingslashit( $page_url ) ) {
			return;
		}

		wp_safe_redirect( $page_url, 301 );
		exit;
	}

}
