<?php
/**
 * Players controller class file.
 *
 * @package Seriously Simple Podcasting
 */

namespace SeriouslySimplePodcasting\Controllers;

use SeriouslySimplePodcasting\Handlers\Options_Handler;
use SeriouslySimplePodcasting\Renderers\Renderer;
use SeriouslySimplePodcasting\Repositories\Episode_Repository;
use SeriouslySimplePodcasting\Traits\Useful_Variables;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Players_Controller class
 *
 * @author      Danilo Radovic, Sergiy Zakharchenko
 * @category    Class
 * @package     SeriouslySimplePodcasting/Controllers
 * @since       2.3
 */
class Players_Controller {

	use Useful_Variables;

	/**
	 * Renderer instance.
	 *
	 * @var Renderer
	 */
	public $renderer;

	/**
	 * Options handler instance.
	 *
	 * @var Options_Handler
	 */
	public $options_handler;

	/**
	 * Episode repository instance.
	 *
	 * @var Episode_Repository
	 */
	public $episode_repository;

	/**
	 * Players_Controller constructor.
	 *
	 * @param Renderer           $renderer           Renderer instance for rendering views.
	 * @param Options_Handler    $options_handler    Handler for plugin options.
	 * @param Episode_Repository $episode_repository Repository for episode data operations.
	 */
	public function __construct( $renderer, $options_handler, $episode_repository ) {
		$this->renderer           = $renderer;
		$this->options_handler    = $options_handler;
		$this->episode_repository = $episode_repository;

		$this->init_useful_variables();
		add_action( 'init', array( $this, 'register_player_assets' ) );
	}

	/**
	 * Registers player assets.
	 *
	 * @return void
	 */
	public function register_player_assets() {

		wp_register_script(
			'ssp-castos-player',
			esc_url( SSP_PLUGIN_URL . 'assets/js/castos-player' . $this->script_suffix . '.js' ),
			array(),
			$this->version,
			true
		);

		wp_register_style(
			'ssp-castos-player',
			esc_url( SSP_PLUGIN_URL . 'assets/css/castos-player' . $this->script_suffix . '.css' ),
			array(),
			$this->version
		);
	}


	/**
	 * Get AJAX playlist items with improved security.
	 *
	 * @return array|void
	 */
	public function get_ajax_playlist_items() {
		// Validate and sanitize input parameters
		$atts_raw      = filter_input( INPUT_GET, 'atts', FILTER_UNSAFE_RAW );
		$page_raw      = filter_input( INPUT_GET, 'page', FILTER_VALIDATE_INT );
		$nonce         = filter_input( INPUT_GET, 'nonce', FILTER_UNSAFE_RAW );
		$player_id_raw = filter_input( INPUT_GET, 'player_id', FILTER_UNSAFE_RAW );

		// Validate required parameters
		if ( empty( $atts_raw ) || empty( $page_raw ) || empty( $nonce ) || empty( $player_id_raw ) ) {
			wp_send_json_error( array( 'message' => 'Missing required parameters' ) );
		}

		// Sanitize and validate atts
		$atts_raw = sanitize_text_field( $atts_raw );
		$atts     = json_decode( $atts_raw, true );
		if ( ! is_array( $atts ) ) {
			wp_send_json_error( array( 'message' => 'Invalid attributes format' ) );
		}

		// Validate page number
		$page = absint( $page_raw );
		if ( $page < 1 || $page > 1000 ) { // Reasonable upper limit
			wp_send_json_error( array( 'message' => 'Invalid page number' ) );
		}

		// Validate player_id format (alphanumeric and hyphens only)
		$player_id = sanitize_key( $player_id_raw );
		if ( empty( $player_id ) || ! preg_match( '/^[a-z0-9-]+$/', $player_id ) ) {
			wp_send_json_error( array( 'message' => 'Invalid player ID format' ) );
		}

		// Verify nonce
		if ( ! wp_verify_nonce( $nonce, 'ssp_castos_player_' . $player_id ) ) {
			wp_send_json_error( array( 'message' => 'Security check failed' ) );
		}

		// Get episodes with validated parameters
		$episodes = $this->episode_repository->get_episodes( array_merge( $atts, compact( 'page' ) ) );
		if ( ! is_array( $episodes ) ) {
			wp_send_json_error( array( 'message' => 'Failed to retrieve episodes' ) );
		}

		$items        = array();
		$allowed_keys = array(
			'episode_id',
			'album_art',
			'podcast_title',
			'title',
			'date',
			'duration',
			'excerpt',
			'audio_file',
		);

		foreach ( $episodes as $episode ) {
			if ( ! is_object( $episode ) || ! isset( $episode->ID ) ) {
				continue;
			}

			$player_data = $this->episode_repository->get_player_data( $episode->ID );
			if ( ! is_array( $player_data ) ) {
				continue;
			}

			// Filter to allowed keys and escape output
			$filtered_data = array_intersect_key( $player_data, array_flip( $allowed_keys ) );

			// Escape all string values
			foreach ( $filtered_data as $key => $value ) {
				if ( is_string( $value ) ) {
					$filtered_data[ $key ] = esc_html( $value );
				} elseif ( is_array( $value ) && isset( $value['src'] ) ) {
					// Handle album_art array
					$filtered_data[ $key ] = array(
						'src' => esc_url( $value['src'] ),
					);
				}
			}

			$items[] = $filtered_data;
		}

		return $items;
	}


	/**
	 * Renders the HTML5 player, based on the attributes sent to the method
	 * If the player assets are registered but not already enqueued, this will enqueue them
	 *
	 * @param int $episode_id Post ID, -1 for current, 0 for latest
	 *
	 * @return string
	 */
	public function render_html_player( $episode_id, $skip_empty_audio = true, $context = 'block', $args = array() ) {
		$template_data = $this->episode_repository->get_player_data( $episode_id, null, false );
		if ( isset( $args['className'] ) ) {
			$template_data['class'] .= ' ' . $args['className'];
		}

		if ( $skip_empty_audio && empty( $template_data['audio_file'] ) ) {
			$show_with_warning = is_admin() ||
								'edit' === filter_input( INPUT_GET, 'context' ) ||
								'elementor' === filter_input( INPUT_GET, 'action' );

			if ( $show_with_warning ) {
				$template_data['add_empty_warning'] = true;
			} else {
				return '';
			}
		}

		$this->enqueue_player_assets();

		$player = $this->renderer->fetch( 'players/castos-player', $template_data );

		$meta = '';

		// For podcast_episode shortcode, we only show meta when explicitly specified with details parameter.
		// Example: [podcast_episode episode="123" content="player,details"].
		$no_meta_contexts = array( 'podcast_episode' );

		$show_meta = ! in_array( $context, $no_meta_contexts );
		$show_meta = apply_filters( 'ssp_show_episode_details', $show_meta, $template_data['episode_id'], $context );

		// Adding 'block' context here for future, to distinguish that request is not from the content filter
		if ( $show_meta ) {
			$meta = $this->episode_meta_details( $template_data['episode_id'], $context );
		}

		return $player . $meta;
	}


	/**
	 * Fetch episode meta details
	 *
	 * @param  integer $episode_id ID of episode post
	 * @param  string  $context    Context for display
	 * @return string              Episode meta details
	 */
	public function episode_meta_details( $episode_id = 0, $context = 'content', $return = false ) {

		if ( ! $episode_id ) {
			return '';
		}

		$file = $this->episode_repository->get_enclosure( $episode_id );

		if ( ! $file ) {
			return '';
		}

		$link = $this->episode_repository->get_episode_download_link( $episode_id, 'download' );

		$duration = get_post_meta( $episode_id, 'duration', true );
		$size     = get_post_meta( $episode_id, 'filesize', true );
		if ( ! $size ) {
			$size_data = $this->episode_repository->get_file_size( $file );
			$size      = isset( $size_data['formatted'] ) ? $size_data['formatted'] : 0;
			if ( $size ) {
				if ( isset( $size_data['formatted'] ) ) {
					update_post_meta( $episode_id, 'filesize', $size_data['formatted'] );
				}

				if ( isset( $size_data['raw'] ) ) {
					update_post_meta( $episode_id, 'filesize_raw', $size_data['raw'] );
				}
			}
		}

		$date_recorded = get_post_meta( $episode_id, 'date_recorded', true );

		// Build up meta data array with default values
		$meta = array(
			'link'          => '',
			'new_window'    => false,
			'duration'      => 0,
			'date_recorded' => '',
		);

		if ( $link ) {
			$meta['link'] = $link;
		}

		if ( $link && apply_filters( 'ssp_show_new_window_link', true, $context ) ) {
			$meta['new_window'] = true;
		}

		if ( $link ) {
			$meta['duration'] = $duration;
		}

		if ( $date_recorded ) {
			$meta['date_recorded'] = $date_recorded;
		}

		// Allow dynamic filtering of meta data - to remove, add or reorder meta items
		$meta = apply_filters( 'ssp_episode_meta_details', $meta, $episode_id, $context );

		if ( true === $return ) {
			return $meta;
		}

		$meta_sep = apply_filters( 'ssp_episode_meta_separator', ' | ' );

		$podcast_display   = $this->get_podcast_display( $meta, $meta_sep );
		$subscribe_display = $this->get_subscribe_display( $episode_id, $context, $meta_sep );
		$meta_display      = $this->get_meta_display( $podcast_display, $subscribe_display, $context );

		return apply_filters( 'ssp_include_player_meta', $meta_display, $episode_id, $context, $meta_sep );
	}


	/**
	 * Renders the Playlist player, based on the attributes sent to the method
	 * If the player assets are registered but not already enqueued, this will enqueue them
	 *
	 * @param array $episodes Array of WP_Post episode objects.
	 * @param array $atts Player attributes.
	 *
	 * @return string
	 */
	public function render_playlist_player( $episodes, $atts ) {
		if ( empty( $episodes ) ) {
			return '';
		}

		// For the case if multiple players are rendered on the same page, we need to generate the player id;
		$player_id = wp_rand();

		wp_localize_script(
			'ssp-castos-player',
			'ssp_castos_player_' . $player_id,
			array(
				'ajax_url' => admin_url( 'admin-ajax.php' ),
				'atts'     => $atts,
				'nonce'    => wp_create_nonce( 'ssp_castos_player_' . $player_id ),
			)
		);
		$this->enqueue_player_assets();

		$template_data = $this->episode_repository->get_player_data( $episodes[0]->ID, get_post() );

		global $wp;
		$template_data['current_url'] = home_url( $wp->request );
		$template_data['player_id']   = $player_id;
		$template_data['class']       = $atts['class'];

		if ( in_array( $atts['style'], array( 'light', 'dark' ) ) ) {
			$template_data['player_mode'] = $atts['style'];
		}

		foreach ( $episodes as $episode ) {
			$template_data['playlist'][] = $this->episode_repository->get_player_data( $episode->ID );
		}

		return $this->renderer->fetch( 'players/castos-player', $template_data );
	}

	/**
	 * Renders the Playlist player, based on the attributes sent to the method
	 * If the player assets are registered but not already enqueued, this will enqueue them
	 *
	 * @param array $tracks
	 * @param array $atts
	 *
	 * @return string
	 */
	public function render_playlist_compact_player( $tracks, $atts, $width, $height ) {

		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return '';
		}

		$this->enqueue_player_assets();

		$data = array(
			'type'         => $atts['type'],
			// don't pass strings to JSON, will be truthy in JS
			'tracklist'    => wp_validate_boolean( $atts['tracklist'] ),
			'tracknumbers' => wp_validate_boolean( $atts['tracknumbers'] ),
			'images'       => wp_validate_boolean( $atts['images'] ),
			'artists'      => false,
			'tracks'       => $tracks,
		);

		$safe_type  = esc_attr( $atts['type'] );
		$safe_style = esc_attr( $atts['style'] );
		$class      = $atts['class'];

		static $instance = 0;
		++$instance;

		if ( 1 === $instance ) {
			/* This hook is defined in wp-includes/media.php */
			do_action( 'wp_playlist_scripts', $atts['type'], $atts['style'] );
		}

		return $this->renderer->fetch(
			'players/playlist-compact-player',
			compact( 'safe_style', 'safe_type', 'data', 'width', 'height', 'class' )
		);
	}

	public function enqueue_player_assets() {
		if ( $this->can_enqueue_script( 'ssp-castos-player' ) ) {
			wp_enqueue_script( 'ssp-castos-player' );
		}
		if ( $this->can_enqueue_style( 'ssp-castos-player' ) ) {
			wp_enqueue_style( 'ssp-castos-player' );
		}

		static $i18n_localized = false;
		if ( ! $i18n_localized && wp_script_is( 'ssp-castos-player', 'enqueued' ) ) {
			wp_localize_script( 'ssp-castos-player', 'ssp_player_i18n', array(
				'playback_speed' => __( 'Playback speed, currently %sx', 'seriously-simple-podcasting' ),
				'copied'         => __( 'Copied', 'seriously-simple-podcasting' ),
			) );
			$i18n_localized = true;
		}

		$is_player_custom_colors_enabled = ssp_app()->get_settings_handler()->is_player_custom_colors_enabled();

		if ( $is_player_custom_colors_enabled && ! wp_style_is( 'ssp-dynamic-style', 'enqueued' ) ) {
			$version = ssp_get_option( 'dynamic_style_version', time() );
			$url     = wp_upload_dir()['baseurl'] . '/ssp/css/ssp-dynamic-style.css';
			wp_enqueue_style(
				'ssp-dynamic-style',
				esc_url( set_url_scheme( $url ) ),
				array( 'ssp-castos-player' ),
				$version
			);
		}
	}

	/**
	 * Checks if the script is registered and not enqueued yet.
	 *
	 * @param string $handle
	 *
	 * @return bool
	 */
	protected function can_enqueue_script( $handle ) {
		return wp_script_is( $handle, 'registered' ) && ! wp_script_is( $handle, 'enqueued' );
	}

	/**
	 * Checks if the style is registered and not enqueued yet.
	 *
	 * @param string $handle
	 *
	 * @return bool
	 */
	protected function can_enqueue_style( $handle ) {
		return wp_style_is( $handle, 'registered' ) && ! wp_style_is( $handle, 'enqueued' );
	}


	/**
	 * Renders the Subscribe Buttons, based on the attributes sent to the method
	 *
	 * @param int $episode_id Episode post ID.
	 *
	 * @return mixed|void
	 */
	public function render_subscribe_buttons( $episode_id ) {
		$subscribe_urls                  = $this->options_handler->get_subscribe_urls( $episode_id, 'subscribe_buttons' );
		$template_data['subscribe_urls'] = $subscribe_urls;

		if ( isset( $template_data['subscribe_urls']['itunes_url'] ) ) {
			$template_data['subscribe_urls']['itunes_url']['label'] = 'Apple Podcast';
			$template_data['subscribe_urls']['itunes_url']['icon']  = 'apple-podcasts.png';
		}

		if ( isset( $template_data['subscribe_urls']['google_play_url'] ) ) {
			$template_data['subscribe_urls']['google_play_url']['label'] = 'Google Podcast';
			$template_data['subscribe_urls']['google_play_url']['icon']  = 'google-podcasts.png';
		}

		$template_data = apply_filters( 'ssp_subscribe_buttons_data', $template_data );

		return $this->renderer->render_deprecated( $template_data, 'players/subscribe-buttons' );
	}

	/**
	 * @param string $src_file
	 * @param int    $episode_id
	 * @param string $player_size
	 * @param string $context
	 *
	 * @return mixed|void
	 */
	public function load_media_player( $src_file, $episode_id, $player_size, $context = 'block' ) {
		// Get episode type and default to audio
		$type = $this->episode_repository->get_episode_type( $episode_id );
		if ( ! $type ) {
			$type = 'audio';
		}

		// Switch to podcast player URL
		$src_file = str_replace( 'podcast-download', 'podcast-player', $src_file );

		// Set up parameters for media player
		$params = array(
			'src'     => $src_file,
			'preload' => 'none',
		);

		/**
		 * If the media file is of type video
		 *
		 * @todo is this necessary in the case of the HTML5 player?
		 */
		if ( 'video' === $type ) {
			// Use featured image as video poster
			if ( $episode_id && has_post_thumbnail( $episode_id ) ) {
				$poster = wp_get_attachment_url( get_post_thumbnail_id( $episode_id ) );
				if ( $poster ) {
					$params['poster'] = $poster;
				}
			}
			$player = wp_video_shortcode( $params );
			// Allow filtering so that alternative players can be used
			return apply_filters( 'ssp_media_player', $player, $src_file, $episode_id );
		}

		/**
		 * Check if this player is being loaded via the AMP for WordPress plugin and if so, force the standard player
		 * https://wordpress.org/plugins/amp/
		 */
		if ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) {
			$player_size = 'standard';
		}

		if ( 'standard' === $player_size ) {
			$player = $this->render_media_player( $episode_id, $context );
		} else {
			$player = $this->render_html_player( $episode_id, true, $context );
		}

		// Allow filtering so that alternative players can be used
		return apply_filters( 'ssp_media_player', $player, $src_file, $episode_id );
	}

	public function media_player( $id ) {
		/**
		 * Get the episode (post) object
		 * If the id passed is empty or 0, get_post will return the current post
		 */
		$episode = get_post( $id );

		$src_file = ssp_episode_passthrough_required( $id ) ?
			$this->episode_repository->get_passthrough_url( $id ) :
			$this->episode_repository->get_enclosure( $id );

		$params = array(
			'src'     => $src_file,
			'preload' => 'none',
		);

		$audio_player = wp_audio_shortcode( $params );
		return array(
			'episode'      => $episode,
			'audio_player' => $audio_player,
		);
	}

	/**
	 * Return media player for a given podcast (episode) id.
	 *
	 * @param int $id
	 *
	 * @return string
	 */
	public function render_media_player( $id, $context = 'block' ) {
		$template_data = $this->media_player( $id );

		$player = $this->renderer->fetch( 'players/media-player', $template_data );

		$meta = '';

		$episode_id = isset( $template_data['episode']->ID ) ? $template_data['episode']->ID : 0;

		// For podcast_episode shortcode, we only show meta when explicitly specified with details parameter.
		// Example: [podcast_episode episode="123" content="player,details"].
		$no_meta_contexts = array( 'podcast_episode' );

		$show_meta = ! in_array( $context, $no_meta_contexts );
		$show_meta = apply_filters( 'ssp_show_episode_details', $show_meta, $episode_id, $context );

		// Adding 'block' context here for future, to distinguish that request is not from the content filter
		if ( $show_meta ) {
			$meta = $this->episode_meta_details( $episode_id, $context );
		}

		return $player . $meta;
	}

	/**
	 * @param array $atts
	 *
	 * @return int[]|\WP_Post[]
	 * @deprecated Use Episode_Repository::get_playlist_episodes()
	 */
	public function get_playlist_episodes( $atts ) {
		return $this->episode_repository->get_episodes( $atts );
	}

	/**
	 * Get the latest episode ID for a player
	 *
	 * @return int
	 * @deprecated Use Episode_Repository::get_latest_episode_id()
	 */
	public function get_latest_episode_id() {
		return $this->episode_repository->get_latest_episode_id();
	}

	/**
	 * @param array  $meta
	 * @param string $meta_sep
	 *
	 * @return string
	 */
	public function get_podcast_display( $meta, $meta_sep ) {
		$podcast_display = '';

		foreach ( $meta as $key => $data ) {
			if ( ! $data ) {
				continue;
			}

			$sep = $podcast_display ? $meta_sep : '';

			switch ( $key ) {
				case 'link':
					if ( 'on' === get_option( 'ss_podcasting_download_file_enabled', 'on' ) ) {
						$podcast_display .= $sep . '<a href="' . esc_url( $data ) . '" title="' . get_the_title() . ' " class="podcast-meta-download">' . __( 'Download file', 'seriously-simple-podcasting' ) . '</a>';
					}
					break;

				case 'new_window':
					if ( isset( $meta['link'] ) && 'on' === get_option( 'ss_podcasting_play_in_new_window_enabled', 'on' ) ) {
						$play_link        = add_query_arg( 'ref', 'new_window', $meta['link'] );
						$podcast_display .= $sep . '<a href="' . esc_url( $play_link ) . '" target="_blank" title="' . get_the_title() . ' " class="podcast-meta-new-window">' . __( 'Play in new window', 'seriously-simple-podcasting' ) . '</a>';
					}

					break;

				case 'duration':
					if ( 'on' === get_option( 'ss_podcasting_duration_enabled', 'on' ) ) {
						$podcast_display .= $sep . '<span class="podcast-meta-duration">' . __( 'Duration', 'seriously-simple-podcasting' ) . ': ' . $data . '</span>';
					}
					break;

				case 'date_recorded':
					if ( 'on' === get_option( 'ss_podcasting_date_recorded_enabled', 'on' ) ) {
						$podcast_display .= $sep . '<span class="podcast-meta-date">' . __( 'Recorded on', 'seriously-simple-podcasting' ) . ' ' . date_i18n( get_option( 'date_format' ), strtotime( $data ) ) . '</span>';
					}
					break;

				// Allow for custom items to be added, but only allow a small amount of HTML tags
				default:
					$allowed_tags     = array(
						'strong' => array(),
						'b'      => array(),
						'em'     => array(),
						'i'      => array(),
						'a'      => array(
							'href'   => array(),
							'title'  => array(),
							'target' => array(),
						),
						'span'   => array(
							'style' => array(),
						),
					);
					$podcast_display .= $sep . wp_kses( $data, $allowed_tags );
					break;
			}
		}

		return $podcast_display;
	}

	/**
	 * @param int    $episode_id
	 * @param string $context
	 * @param string $meta_sep
	 *
	 * @return string
	 */
	public function get_subscribe_display( $episode_id, $context, $meta_sep ) {
		$subscribe_display = '';
		if ( 'on' !== get_option( 'ss_podcasting_player_subscribe_urls_enabled', 'on' ) ) {
			return $subscribe_display;
		}
		$options_handler = new Options_Handler();
		$subscribe_urls  = $options_handler->get_subscribe_urls( $episode_id, $context );
		foreach ( $subscribe_urls as $key => $data ) {
			if ( empty( $data['url'] ) ) {
				continue;
			}

			if ( $subscribe_display ) {
				$subscribe_display .= $meta_sep;
			}

			if ( preg_match( '/\b_url\b/', $key ) === false ) {
				$allowed_tags       = array(
					'strong' => array(),
					'b'      => array(),
					'em'     => array(),
					'i'      => array(),
					'a'      => array(
						'href'   => array(),
						'title'  => array(),
						'target' => array(),
					),
				);
				$subscribe_display .= wp_kses( $data['url'], $allowed_tags );
			} else {
				$subscribe_display .= '<a href="' . esc_url( $data['url'] ) . '" target="_blank" title="' . $data['label'] . '" class="podcast-meta-itunes">' . $data['label'] . '</a>';
			}
		}

		return $subscribe_display;
	}

	/**
	 * @param string $podcast_display
	 * @param string $subscribe_display
	 *
	 * @return string
	 */
	public function get_meta_display( $podcast_display, $subscribe_display, $context = '' ) {
		$meta_display = '';

		if ( ! $podcast_display && ! $subscribe_display ) {
			return $meta_display;
		}

		if ( ! empty( $podcast_display ) || ! empty( $subscribe_display ) ) {
			$meta_display .= '<div class="podcast_meta"><aside>';

			$ss_podcasting_player_meta_data_enabled = get_option( 'ss_podcasting_player_meta_data_enabled', 'on' );

			if ( 'on' === $ss_podcasting_player_meta_data_enabled || 'shortcode' === $context ) {
				if ( ! empty( $podcast_display ) ) {
					$podcast_display = '<p>' . $podcast_display . '</p>';
					$podcast_display = apply_filters( 'ssp_include_episode_meta_data', $podcast_display );
					if ( $podcast_display && ! empty( $podcast_display ) ) {
						$meta_display .= $podcast_display;
					}
				}
			}

			if ( ! empty( $subscribe_display ) ) {
				$subscribe_display = '<p>' . __( 'Subscribe:', 'seriously-simple-podcasting' ) . ' ' . $subscribe_display . '</p>';
				$subscribe_display = apply_filters( 'ssp_include_podcast_subscribe_links', $subscribe_display );
				if ( $subscribe_display && ! empty( $subscribe_display ) ) {
					$meta_display .= $subscribe_display;
				}
			}

			$meta_display .= '</aside></div>';
		}

		return $meta_display;
	}
}
