<?php

namespace SeriouslySimplePodcasting\Controllers;

use SeriouslySimplePodcasting\Entities\Castos_File_Data;
use SeriouslySimplePodcasting\Entities\Sync_Status;
use SeriouslySimplePodcasting\Handlers\Admin_Notifications_Handler;
use SeriouslySimplePodcasting\Handlers\CPT_Podcast_Handler;
use SeriouslySimplePodcasting\Handlers\Castos_Handler;
use SeriouslySimplePodcasting\Handlers\Feed_Handler;
use SeriouslySimplePodcasting\Handlers\Podping_Handler;
use SeriouslySimplePodcasting\Handlers\Series_Handler;
use SeriouslySimplePodcasting\Handlers\UUID_Handler;
use SeriouslySimplePodcasting\Repositories\Episode_Repository;
use SeriouslySimplePodcasting\Traits\Useful_Variables;


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

/**
 * This is controller for Podcast and other SSP post types (which are enabled via settings) custom behavior.
 *
 * @category    Class
 * @package     SeriouslySimplePodcasting/Controllers
 * @since       2.13.0
 */
class Podcast_Post_Types_Controller {

	use Useful_Variables;


	/**
	 * @var CPT_Podcast_Handler
	 */
	protected $cpt_podcast_handler;

	/**
	 * @var Castos_Handler
	 * */
	protected $castos_handler;

	/**
	 * @var Admin_Notifications_Handler
	 */
	protected $admin_notices_handler;

	/**
	 * @var Podping_Handler
	 */
	protected $podping_handler;

	/**
	 * @var Episode_Repository
	 */
	protected $episode_repository;

	/**
	 * @var Series_Handler
	 */
	protected $series_handler;

	/**
	 * @param CPT_Podcast_Handler         $cpt_podcast_handler
	 * @param Castos_Handler              $castos_handler
	 * @param Admin_Notifications_Handler $admin_notices_handler
	 * @param Podping_Handler             $podping_handler
	 * @param Episode_Repository          $episode_repository
	 * @param Series_Handler              $series_handler
	 */
	public function __construct(
		$cpt_podcast_handler,
		$castos_handler,
		$admin_notices_handler,
		$podping_handler,
		$episode_repository,
		$series_handler
	) {
		$this->cpt_podcast_handler   = $cpt_podcast_handler;
		$this->castos_handler        = $castos_handler;
		$this->admin_notices_handler = $admin_notices_handler;
		$this->podping_handler       = $podping_handler;
		$this->episode_repository    = $episode_repository;
		$this->series_handler        = $series_handler;

		$this->init_useful_variables();
		$this->register_hooks_and_filters();
	}

	/**
	 * Register all relevant front end hooks and filters
	 */
	protected function register_hooks_and_filters() {

		// Register podcast post type, taxonomies and meta fields.
		add_action( 'init', array( $this, 'register_post_type' ), 11 );

		// Update podcast post type arguments to include custom-fields support if not already enabled.
		add_filter( 'register_post_type_args', array( $this, 'update_podcast_post_type_args' ), 20, 2 );

		// Prevent copying some meta fields
		add_action( 'admin_init', array( $this, 'prevent_copy_meta' ) );

		// Episode meta box.
		add_action( 'admin_init', array( $this, 'register_meta_boxes' ) );
		add_action( 'save_post', array( $this, 'meta_box_save' ), 10, 2 );

		// Clear the cache on post save.
		add_action( 'save_post', array( $this, 'invalidate_cache' ), 10, 2 );

		// Update podcast details to Castos when a post is updated or saved
		add_action( 'save_post', array( $this, 'sync_episode' ), 20, 2 );
		add_action( 'et_save_post', array( $this, 'sync_divi_episode' ) );
		add_action( 'elementor/editor/after_save', array( $this, 'sync_elementor_episode' ) );

		// Assign default series if no series was specified
		add_action( 'save_post', array( $this, 'maybe_assign_default_series' ), 20 );

		// Notify Podping if new episode has been published, or if new series is assigned to the episode
		add_action( 'wp_after_insert_post', array( $this, 'notify_podping' ), 10, 4 );
		add_action( 'added_term_relationship', array( $this, 'notify_podping_on_series_added' ), 10, 3 );

		// Delete podcast from Castos
		add_action( 'trashed_post', array( $this, 'delete_post' ), 11 );

		// Episode edit screen.
		add_filter( 'enter_title_here', array( $this, 'enter_title_here' ) );
		add_filter( 'post_updated_messages', array( $this, 'updated_messages' ) );

		// Episodes list table.
		add_action( 'admin_init', array( $this, 'add_custom_columns' ) );

		// Change the podcast episode statuses to sending after the sync has been triggered.
		add_action( 'ssp_triggered_podcast_sync', array( $this, 'update_podcast_episodes_status' ), 10, 2 );

		add_action( 'ssp_check_episode_sync_status', array( $this, 'maybe_update_sync_status' ), 10, 2 );

		// Handle translations
		add_filter( 'pll_copy_post_metas', array( $this, 'handle_polylang_translations' ), 10, 3 );
	}

	/**
	 * Updates podcast post type arguments to include custom-fields support if not already enabled.
	 * Custom fields are necessary for displaying both standard and Gutenberg meta fields.
	 *
	 * @since 3.7.0
	 *
	 * @param array  $args
	 * @param string $post_type
	 *
	 * @return array
	 */
	public function update_podcast_post_type_args( $args, $post_type ) {
		$podcast_post_types = get_option( 'ss_podcasting_use_post_types', array() );

		if ( ! is_array( $podcast_post_types ) ||
			! is_array( $args ) ||
			! in_array( $post_type, $podcast_post_types ) ||
			! empty( $args['supports']['custom-fields'] ) ) {
			return $args;
		}

		if ( empty( $args['supports'] ) ) {
			// Add default support values if none are defined.
			$args['supports'] = array( 'title', 'editor', 'autosave' );
		}
		$args['supports'][] = 'custom-fields';

		return $args;
	}

	/**
	 * Handle Polylang translations - prevent copying episode meta fields.
	 *
	 * @param array $metas
	 *
	 * @return array
	 */
	public function handle_polylang_translations( $metas, $sync, $post_id ) {
		$post = get_post( $post_id );
		if ( ! in_array( $post->post_type, ssp_post_types() ) ) {
			return $metas;
		}
		$fields  = array_keys( $this->custom_fields( true ) );
		$fields  = array_merge(
			$fields,
			array(
				'enclosure',
				'castos_sync_attempts',
				'podmotor_episode_id',
				'podmotor_file_id',
			)
		);
		$allowed = array( 'cover_image', 'cover_image_id' );
		$fields  = array_diff( $fields, $allowed );

		return array_diff( $metas, $fields );
	}

	public function add_custom_columns() {
		$ssp_post_types = ssp_post_types();
		foreach ( $ssp_post_types as $post_type ) {
			add_filter( 'manage_edit-' . $post_type . '_columns', array( $this, 'register_custom_column_headings' ), 20, 2 );
		}
		add_action( 'manage_posts_custom_column', array( $this, 'manage_custom_columns' ), 10, 2 );
	}


	/**
	 * Register SSP_CPT_PODCAST post type
	 *
	 * @return void
	 */
	public function register_post_type() {
		$this->cpt_podcast_handler->register_post_type();
	}

	/**
	 * @param int    $episode_id
	 * @param string $episode_status
	 *
	 * @return void
	 * @throws \Exception
	 */
	public function maybe_update_sync_status( $episode_id, $episode_status ) {
		$series_id      = ssp_get_episode_series_id( $episode_id );
		$podcast_status = $this->series_handler->get_sync_status( $series_id );
		$syncing        = Sync_Status::SYNC_STATUS_SYNCING;

		// First, let's check if the podcast is still syncing and maybe update the status
		if ( $syncing === $podcast_status || ! $podcast_status ) {
			$sync_status    = $this->castos_handler->get_podcast_sync_status( $series_id );
			$podcast_status = $sync_status->status;
			$this->series_handler->update_sync_status( $series_id, $podcast_status );
		}

		if ( $syncing == $episode_status && $podcast_status && $syncing != $podcast_status ) {
			// If the podcast status is synced_with_errors, and episode status is syncing, it's failed.
			$new_episode_status = Sync_Status::SYNC_STATUS_SYNCED_WITH_ERRORS === $podcast_status ?
				Sync_Status::SYNC_STATUS_FAILED : $podcast_status;
			$this->episode_repository->update_episode_sync_status( $episode_id, $new_episode_status );
		}
	}

	/**
	 * @param int   $podcast_id
	 * @param array $response
	 */
	public function update_podcast_episodes_status( $podcast_id, $response ) {
		if ( isset( $response['code'] ) && 200 === $response['code'] ) {
			$episodes = $this->episode_repository->get_podcast_episodes( $podcast_id );

			foreach ( $episodes as $episode ) {
				$this->episode_repository->update_episode_sync_status(
					$episode->ID,
					Sync_Status::SYNC_STATUS_SYNCING
				);
			}
		}
	}

	/**
	 * This function fires in such cases:
	 *  - if a new episode with series is published
	 *  - if a new series is added to existing episode.
	 *
	 * @param int    $post_id
	 * @param int    $term_id
	 * @param string $taxonomy
	 *
	 * @return bool
	 */
	public function notify_podping_on_series_added( $post_id, $term_id, $taxonomy ) {

		$post = get_post( $post_id );

		if ( ssp_series_taxonomy() !== $taxonomy || 'publish' !== $post->post_status ) {
			return false;
		}

		if ( ! get_option( 'blog_public' ) || ! ssp_get_option( 'podping_notification', 'on', $term_id ) ) {
			return false;
		}

		// If this action was fired, it means that we don't need to fire notify_podping() anymore
		remove_action( 'wp_after_insert_post', array( $this, 'notify_podping' ) );

		if ( ! in_array( $post->post_type, ssp_post_types( true ), true ) ) {
			return false;
		}

		$term = get_term_by( 'id', $term_id, $taxonomy );

		if ( empty( $term->slug ) ) {
			return false;
		}

		$feed_url = ssp_get_feed_url( $term->slug );

		return $this->podping_handler->notify( $feed_url );
	}

	/**
	 * This function is needed for such cases:
	 *  - when a new episode without series is created
	 *  - when a new episode with series was first created as draft and then published.
	 * For all other cases, @param \WP_Post $post
	 *
	 * @see notify_podping_on_series_added()
	 */
	public function notify_podping( $post_id, $post, $update, $post_before ) {

		$is_just_published = isset( $post_before->post_status ) && isset( $post->post_status ) &&
							'publish' !== $post_before->post_status && 'publish' === $post->post_status;

		if ( ! $is_just_published ) {
			return;
		}

		if ( ! in_array( $post->post_type, ssp_post_types( true ), true ) || ! get_option( 'blog_public' ) ) {
			return;
		}

		$series_terms = wp_get_post_terms( $post->ID, ssp_series_taxonomy() );
		$feed_urls    = array();

		/**
		 * Episode can belong to multiple series feeds, so let's notify all of them.
		 * If episode doesn't belong to any series, it belongs to the main feed.
		 *
		 * @var \WP_Term[] $series_terms
		 * */
		if ( is_array( $series_terms ) && $series_terms ) {
			// This is the case when episode with series was saved first as draft and then published.
			foreach ( $series_terms as $term ) {
				$is_notification_enabled = ssp_get_option( 'podping_notification', 'on', $term->term_id );
				if ( $is_notification_enabled ) {
					$feed_urls[] = ssp_get_feed_url( $term->slug );
				}
			}
		} else {
			// This is the case when a new episode without series was published.
			$is_notification_enabled = ssp_get_option( 'podping_notification', 'on' );
			if ( $is_notification_enabled ) {
				$feed_urls[] = ssp_get_feed_url();
			}
		}

		foreach ( $feed_urls as $feed_url ) {
			$this->podping_handler->notify( $feed_url );
		}
	}


	/**
	 * Delete the podcast from Castos
	 *
	 * @param int $post_id Post ID.
	 */
	public function delete_post( $post_id ) {
		$post = get_post( $post_id );

		if ( ! in_array( $post->post_type, ssp_post_types(), true ) ) {
			return;
		}

		if ( ssp_is_connected_to_castos() ) {
			$this->castos_handler->delete_episode( $post );
		}

		$this->episode_repository->delete_sync_info( $post_id );
		$this->episode_repository->delete_audio_file( $post_id );
	}


	/**
	 * Prevents copying some episode meta fields.
	 */
	public function prevent_copy_meta() {
		add_action(
			'wp_insert_post',
			function ( $post_id, $post, $update ) {
				if ( $update || ! in_array( $post->post_type, ssp_post_types() ) ) {
					return;
				}

				$remove_redundant_metas = function ( $post_id ) {
					$exclusions = array(
						'podmotor_file_id',
						'podmotor_episode_id',
						'audio_file',
						'enclosure',
						'castos_file_data',
						'date_recorded',
						'duration',
						'filesize',
						'filesize_raw',
						'itunes_episode_number',
						'sync_status',
						'transcript_file',
						'ssp_guid',
						'ssp_original_guid',
					);

					foreach ( $exclusions as $exclusion ) {
						delete_post_meta( $post_id, $exclusion );
					}
				};

				// Most of the copy plugins use redirection after creating the post and it's meta.
				add_filter(
					'wp_redirect',
					function ( $location ) use ( $remove_redundant_metas, $post_id ) {
						$remove_redundant_metas( $post_id );

						return $location;
					}
				);

				// This is for Post Duplicator plugin
				add_action(
					'mtphr_post_duplicator_created',
					function () use ( $remove_redundant_metas, $post_id ) {
						$remove_redundant_metas( $post_id );
					}
				);
			},
			10,
			3
		);
	}


	/**
	 * Register podcast episode details meta boxes
	 *
	 * @return void
	 */
	public function register_meta_boxes() {

		// Get all podcast post types
		$podcast_post_types = ssp_post_types( true );

		// Add meta box to each post type
		foreach ( (array) $podcast_post_types as $post_type ) {
			add_action( 'add_meta_boxes_' . $post_type, array( $this, 'meta_box_setup' ), 10, 1 );
		}
	}

	/**
	 * @param int $post_id
	 *
	 * @return void
	 */
	public function maybe_assign_default_series( $post_id ) {
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}
		$post = get_post( $post_id );
		if ( ! in_array( $post->post_type, ssp_post_types() ) ) {
			return;
		} elseif ( $post->post_type != SSP_CPT_PODCAST ) {
			// If it's a not a podcast post, make sure it has the enclosure file.
			if ( ! $this->episode_repository->get_enclosure( $post_id ) ) {
				return;
			}
		}

		$series_id = ssp_get_episode_series_id( $post_id, 0 );
		if ( ! $series_id ) {
			$default_series_id = ssp_get_default_series_id();
			wp_set_object_terms( $post_id, array( $default_series_id ), ssp_series_taxonomy() );
		}
	}

	/**
	 * Save episode meta box content
	 *
	 * @param integer  $post_id ID of post
	 * @param \WP_Post $post
	 *
	 * @return bool
	 */
	public function meta_box_save( $post_id, $post ) {

		if ( ! $this->save_podcast_action_check( $post ) ) {
			return false;
		}

		// User capability check
		if ( 'page' == $_POST['post_type'] ) {
			if ( ! current_user_can( 'edit_page', $post_id ) ) {
				return $post_id;
			}
		} elseif ( ! current_user_can( 'edit_post', $post_id ) ) {
				return $post_id;
		}

		$field_data = $this->custom_fields();

		$old_data = array();

		$enclosure = '';

		foreach ( $field_data as $k => $field ) {
			if ( 'embed_code' == $k ) {
				continue;
			}

			$val = '';
			if ( isset( $_POST[ $k ] ) ) {
				if ( isset( $field['callback'] ) ) {
					$val = call_user_func( $field['callback'], $_POST[ $k ] );
				} else {
					$val = strip_tags( trim( $_POST[ $k ] ) );
				}
			}

			if ( $k == 'audio_file' ) {
				$enclosure = $val;
			}

			$old_data[ $k ] = get_post_meta( $post_id, $k, true );

			if ( $old_data[ $k ] !== $val ) {
				update_post_meta( $post_id, $k, $val );
			}
		}

		if ( $enclosure ) {
			$this->handle_enclosure_update( $post, $enclosure );

			// Generate UUID-based GUID for new episodes (backward compatibility)
			$this->handle_guid( $post_id );
		}

		return true;
	}

	/**
	 * Checks if the post is a podcast post type and has a valid nonce.
	 *
	 * @param \WP_Post $post
	 *
	 * @return bool
	 */
	public function save_podcast_action_check( $post ) {
		$podcast_post_types = ssp_post_types();

		// Post type check
		if ( ( 'trash' === $post->post_status ) || ! in_array( $post->post_type, $podcast_post_types ) ) {
			return false;
		}

		// Security check
		if ( ! isset( $_POST[ 'seriouslysimple_' . $this->token . '_nonce' ] ) ||
			! wp_verify_nonce( $_POST[ 'seriouslysimple_' . $this->token . '_nonce' ], plugin_basename( $this->dir ) )
		) {
			return false;
		}

		return true;
	}

	/**
	 * Handle enclosure update for podcast episodes
	 *
	 * @param \WP_Post $post      Post object.
	 * @param string   $enclosure New enclosure URL.
	 *
	 * @return void
	 */
	public function handle_enclosure_update( $post, $enclosure ) {
		$post_id              = $post->ID;
		$old_enclosure        = get_post_meta( $post_id, 'audio_file', true );
		$is_enclosure_updated = $old_enclosure !== $enclosure;

		// Update recorded date if enclosure changed or date is empty
		if ( $is_enclosure_updated || get_post_meta( $post_id, 'date_recorded', true ) == '' ) {
			update_post_meta( $post_id, 'date_recorded', $post->post_date );
		}

		// Save podcast file to the 'enclosure' meta field (WP-standard format) for standards-sake.
		// When the URL changed, force size 0 so the previous file's size is not paired with the new
		// URL; self-hosted refreshes it below once filesize_raw is recalculated, and the Castos REST
		// sync callback refreshes it for Castos sites.
		$this->update_enclosure_meta( $post_id, $enclosure, $is_enclosure_updated ? 0 : null );

		// Only process file metadata if not connected to Castos
		if ( ssp_is_connected_to_castos() ) {
			return;
		}

		// Get file duration
		if ( $is_enclosure_updated || get_post_meta( $post_id, 'duration', true ) == '' ) {
			$duration = $this->episode_repository->get_file_duration( $enclosure );
			if ( $duration ) {
				update_post_meta( $post_id, 'duration', $duration );
			}
		}

		// Get file size
		// Only skip if BOTH filesize and filesize_raw exist (ensures data consistency)
		// If either is missing, recalculate both to avoid mixing frontend/backend data
		$has_filesize     = get_post_meta( $post_id, 'filesize', true ) != '';
		$has_filesize_raw = get_post_meta( $post_id, 'filesize_raw', true ) != '';

		if ( $is_enclosure_updated || ! $has_filesize || ! $has_filesize_raw ) {
			$filesize = $this->episode_repository->get_file_size( $enclosure );
			if ( $filesize ) {
				if ( isset( $filesize['formatted'] ) ) {
					update_post_meta( $post_id, 'filesize', $filesize['formatted'] );
				}

				if ( isset( $filesize['raw'] ) ) {
					update_post_meta( $post_id, 'filesize_raw', $filesize['raw'] );

					// Refresh the enclosure meta now that the real file size is known.
					$this->update_enclosure_meta( $post_id, $enclosure, $filesize['raw'] );
				}
			}
		}
	}

	/**
	 * Writes the 'enclosure' meta in WordPress's standard "url\nsize\nmime\n" format.
	 *
	 * When $size is null, falls back to the stored filesize_raw meta (populated for both
	 * self-hosted and Castos episodes). Callers pass an explicit size to avoid pairing a
	 * stale size with a changed URL, keeping the value parseable by external podcasting tools.
	 *
	 * @param int             $post_id Episode post ID.
	 * @param string          $url     Enclosure file URL.
	 * @param int|string|null $size    Raw file size; null reads it from the filesize_raw meta.
	 *
	 * @return void
	 */
	protected function update_enclosure_meta( $post_id, $url, $size = null ) {
		if ( null === $size ) {
			$size = get_post_meta( $post_id, 'filesize_raw', true );
		}

		$enclosure = $this->episode_repository->format_enclosure( $url, $size );
		update_post_meta( $post_id, 'enclosure', $enclosure );
	}

	/**
	 * Handle GUID generation for new episodes if one doesn't exist
	 *
	 * @param int $post_id Post ID.
	 *
	 * @return void
	 */
	public function handle_guid( $post_id ) {
		// Check if GUID already exists (backward compatibility)
		$existing_guid = get_post_meta( $post_id, 'ssp_guid', true );
		if ( $existing_guid ) {
			return;
		}

		$episode = get_post( $post_id );
		if ( ! $episode ) {
			return;
		}

		$guid = ssp_generate_episode_guid( $episode );

		if ( $guid ) {
			// Save the GUID
			update_post_meta( $post_id, 'ssp_guid', $guid );
		}
	}

	/**
	 * Create meta box on episode edit screen
	 *
	 * @return void
	 */
	public function meta_box_setup( $post ) {
		global $pagenow;
		add_meta_box(
			'podcast-episode-data',
			__( 'Podcast Episode Details', 'seriously-simple-podcasting' ),
			array(
				$this,
				'meta_box_content',
			),
			$post->post_type,
			'normal',
			'high'
		);

		if ( 'post.php' == $pagenow && 'publish' == $post->post_status && function_exists( 'get_post_embed_html' ) ) {
			add_meta_box(
				'episode-embed-code',
				__( 'Episode Embed Code', 'seriously-simple-podcasting' ),
				array(
					$this,
					'embed_code_meta_box_content',
				),
				$post->post_type,
				'side',
				'low'
			);
		}

		// Allow more metaboxes to be added
		do_action( 'ssp_meta_boxes', $post );
	}

	/**
	 * Get content for episode embed code meta box
	 *
	 * @param object $post Current post object
	 *
	 * @return void
	 */
	public function embed_code_meta_box_content( $post ) {

		// Get post embed code
		$embed_code = get_post_embed_html( 500, 350, $post );

		// Generate markup for meta box
		$html  = '<p><em>' . __( 'Customise the size of your episode embed below, then copy the HTML to your clipboard.', 'seriously-simple-podcasting' ) . '</em></p>';
		$html .= '<p><label for="episode_embed_code_width">' . __( 'Width:', 'seriously-simple-podcasting' ) . '</label> <input id="episode_embed_code_width" class="episode_embed_code_size_option" type="number" value="500" length="3" min="0" step="1" /> &nbsp;&nbsp;&nbsp;&nbsp;<label for="episode_embed_code_height">' . __( 'Height:', 'seriously-simple-podcasting' ) . '</label> <input id="episode_embed_code_height" class="episode_embed_code_size_option" type="number" value="350" length="3" min="0" step="1" /></p>';
		$html .= '<p><textarea readonly id="episode_embed_code">' . esc_textarea( $embed_code ) . '</textarea></p>';

		echo $html;
	}

	/**
	 * Load content for episode meta box
	 *
	 * @param \WP_Post $post
	 *
	 * @return void
	 */
	public function meta_box_content( $post ) {

		$post_id = $post->ID;

		$field_data = $this->custom_fields();

		$html = '<input type="hidden" name="seriouslysimple_' . $this->token . '_nonce" id="seriouslysimple_' . $this->token . '_nonce" value="' . wp_create_nonce( plugin_basename( $this->dir ) ) . '" />';

		if ( 0 < count( $field_data ) ) {
			$html    .= '<input id="seriouslysimple_post_id" type="hidden" value="' . $post_id . '" />';
			$renderer = ssp_renderer();

			foreach ( $field_data as $k => $v ) {
				$data  = isset( $v['default'] ) ? $v['default'] : '';
				$saved = get_post_meta( $post_id, $k, true );
				if ( $saved ) {
					$data = $saved;
				}

				$class = '';
				if ( isset( $v['class'] ) ) {
					$class = $v['class'];
				}

				switch ( $v['type'] ) {
					case 'sync_status':
						try {
							$status = $this->episode_repository->get_episode_sync_status( $post_id );
						} catch ( \Exception $e ) {
							$status = Sync_Status::SYNC_STATUS_NONE;
						}
						$html .= $renderer->fetch( 'metafields/sync-status', compact( 'status' ) );
						break;

					case 'file':
						$html .= $renderer->fetch( 'metafields/file', compact( 'k', 'v', 'data' ) );
						break;

					case 'episode_file':
						$is_castos = ssp_is_connected_to_castos();
						$file_data = new Castos_File_Data(
							json_decode( get_post_meta( $post_id, 'castos_file_data', true ), true )
						);
						$html     .= $renderer->fetch(
							'metafields/episode_file',
							compact( 'k', 'v', 'data', 'is_castos', 'file_data' )
						);
						break;

					case 'image':
						$label       = $v['name'];
						$description = $v['description'];
						$validator   = isset( $v['validator'] ) ? $v['validator'] : '';
						$html       .= $renderer->fetch( 'metafields/image', compact( 'label', 'description', 'validator', 'data', 'k' ) );
						break;

					case 'checkbox':
						$html .= $renderer->fetch( 'metafields/checkbox', compact( 'k', 'v', 'class', 'data' ) );
						break;

					case 'radio':
						$html .= $renderer->fetch( 'metafields/radio', compact( 'k', 'v', 'class', 'data' ) );
						break;

					case 'select':
						$html .= $renderer->fetch( 'metafields/select', compact( 'k', 'v', 'class', 'data' ) );
						break;

					case 'datepicker':
						$html .= $renderer->fetch( 'metafields/datepicker', compact( 'k', 'v', 'class', 'data' ) );
						break;

					case 'textarea':
						$html .= $renderer->fetch( 'metafields/textarea', compact( 'k', 'v', 'class', 'data' ) );
						break;

					case 'hidden':
						$html .= $renderer->fetch( 'metafields/hidden', compact( 'k', 'v', 'class', 'data' ) );
						break;

					case 'number':
						$html .= $renderer->fetch( 'metafields/number', compact( 'k', 'v', 'class', 'data' ) );
						break;

					default:
						$html .= $renderer->fetch( 'metafields/text', compact( 'k', 'v', 'class', 'data' ) );
						break;
				}
			}
		}

		echo $html;
	}

	/**
	 * Setup custom fields for episodes
	 *
	 * @param bool $all
	 *
	 * @return array Custom fields
	 */
	public function custom_fields( $all = false ) {
		return $this->cpt_podcast_handler->custom_fields( $all );
	}

	/**
	 * Syncs a Divi episode with Castos
	 *
	 * @since 3.8.0
	 *
	 * @param int $post_id Post ID.
	 *
	 * @return void
	 */
	public function sync_divi_episode( $post_id ) {
		// Post type check
		$post = get_post( $post_id );

		if ( 'publish' !== $post->post_status ) {
			return;
		}

		$podcast_post_types = ssp_post_types();

		if ( ! in_array( $post->post_type, $podcast_post_types ) || ! ssp_is_connected_to_castos() ) {
			return;
		}

		$get_divi_content = function ( $content, $post ) {
			if ( ! $content ) {
				$content = $post->post_content;
			}
			if ( false !== strpos( $content, '[et_pb_' ) ) {
				$content = do_shortcode( $content );
			}

			return $content;
		};

		add_filter( 'ssp_feed_item_raw_content', $get_divi_content, 10, 2 );

		$this->upload_episode_to_castos( $post );

		remove_filter( 'ssp_feed_item_raw_content', $get_divi_content );
	}

	/**
	 * Syncs an Elementor episode with Castos
	 *
	 * @since 3.8.0
	 *
	 * @param int $post_id Post ID.
	 *
	 * @return void
	 */
	public function sync_elementor_episode( $post_id ) {
		// Post type check
		$post = get_post( $post_id );

		if ( 'publish' !== $post->post_status ) {
			return;
		}

		$podcast_post_types = ssp_post_types();

		if ( ! in_array( $post->post_type, $podcast_post_types ) || ! ssp_is_connected_to_castos() ) {
			return;
		}

		$this->upload_episode_to_castos( $post );
	}

	/**
	 * Send the podcast details to Castos
	 *
	 * @param int      $id
	 * @param \WP_Post $post
	 */
	public function sync_episode( $id, $post ) {
		/**
		 * Don't trigger this if we're not connected to Castos
		 */
		if ( ! ssp_is_connected_to_castos() ) {
			return;
		}

		if ( ! $this->save_podcast_action_check( $post ) ) {
			return;
		}

		/**
		 * Only trigger this if the post is published or scheduled
		 */
		$disallowed_statuses = array( 'draft', 'pending', 'private', 'trash', 'auto-draft' );
		if ( in_array( $post->post_status, $disallowed_statuses, true ) ) {
			return;
		}

		if ( empty( $post->ID ) || empty( $post->post_title ) ) {
			return;
		}

		/**
		 * Don't trigger this unless we have a valid castos file id
		 */
		if ( empty( ssp_castos_file_id( $post->ID ) ) ) {
			return;
		}

		$this->episode_repository->delete_episode_sync_error( $post->ID );

		$this->upload_episode_to_castos( $post );
	}


	/**
	 * Uploads an episode to Castos and updates the episode's sync post metadata.
	 * Schedules a sync by cron in case of a sync error.
	 *
	 * @since 3.8.0
	 *
	 * @param \WP_Post $post Post object.
	 *
	 * @return void
	 */
	protected function upload_episode_to_castos( $post ) {
		/**
		 * Don't trigger this unless we have a valid file id
		 */
		if ( empty( ssp_castos_file_id( $post->ID ) ) ) {
			return;
		}

		$response = $this->castos_handler->upload_episode_to_castos( $post );

		if ( $response->success ) {
			if ( $response->castos_episode_id ) {
				update_post_meta( $post->ID, 'podmotor_episode_id', $response->castos_episode_id );
			}
			$this->admin_notices_handler->add_flash_notice(
				$response->message,
				Admin_Notifications_Handler::SUCCESS
			);

			// if uploading was scheduled before, lets unschedule it
			delete_post_meta( $post->ID, 'podmotor_schedule_upload' );
			$this->episode_repository->update_episode_sync_status( $post->ID, Sync_Status::SYNC_STATUS_SYNCED );
			$this->episode_repository->delete_sync_error( $post->ID );
		} else {
			// Schedule uploading with a cronjob.
			// If it's 404, something wrong with the file ID. We don't try to reupload it since result will be the same.
			if ( in_array( $response->code, array( 403, 404 ) ) ) {
				$this->admin_notices_handler->add_flash_notice(
					$response->message,
					Admin_Notifications_Handler::ERROR
				);
			} else {
				update_post_meta( $post->ID, Cron_Controller::SYNC_SCHEDULE_META, true );
				update_post_meta( $post->ID, Cron_Controller::ATTEMPTS_META, 1 );
				$this->admin_notices_handler->add_predefined_flash_notice(
					Admin_Notifications_Handler::NOTICE_API_EPISODE_ERROR
				);
			}

			$this->episode_repository->update_episode_sync_status( $post->ID, Sync_Status::SYNC_STATUS_FAILED );
			$this->episode_repository->update_episode_sync_error( $post->ID, $response->message );
		}
	}

	/**
	 * Modify the 'enter title here' text
	 *
	 * @param string $title Default text
	 *
	 * @return string        Modified text
	 */
	public function enter_title_here( $title ) {
		if ( get_post_type() == $this->token ) {
			$title = __( 'Enter episode title here', 'seriously-simple-podcasting' );
		}

		return $title;
	}

	/**
	 * Create custom dashboard message
	 *
	 * @param array $messages Default messages
	 *
	 * @return array           Modified messages
	 */
	public function updated_messages( $messages ) {
		global $post, $post_ID;

		$messages[ $this->token ] = array(
			0  => '',
			1  => sprintf( __( 'Episode updated. %1$sView episode%2$s.', 'seriously-simple-podcasting' ), '<a href="' . esc_url( get_permalink( $post_ID ) ) . '">', '</a>' ),
			2  => __( 'Custom field updated.', 'seriously-simple-podcasting' ),
			3  => __( 'Custom field deleted.', 'seriously-simple-podcasting' ),
			4  => __( 'Episode updated.', 'seriously-simple-podcasting' ),
			5  => isset( $_GET['revision'] ) ? sprintf( __( 'Episode restored to revision from %s.', 'seriously-simple-podcasting' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
			6  => sprintf( __( 'Episode published. %1$sView episode%2$s.', 'seriously-simple-podcasting' ), '<a href="' . esc_url( get_permalink( $post_ID ) ) . '">', '</a>' ),
			7  => __( 'Episode saved.', 'seriously-simple-podcasting' ),
			8  => sprintf( __( 'Episode submitted. %1$sPreview episode%2$s.', 'seriously-simple-podcasting' ), '<a target="_blank" href="' . esc_url( add_query_arg( 'preview', 'true', get_permalink( $post_ID ) ) ) . '">', '</a>' ),
			9  => sprintf( __( 'Episode scheduled for: %1$s. %2$sPreview episode%3$s.', 'seriously-simple-podcasting' ), '<strong>' . date_i18n( __( 'M j, Y @ G:i', 'seriously-simple-podcasting' ), strtotime( $post->post_date ) ) . '</strong>', '<a target="_blank" href="' . esc_url( get_permalink( $post_ID ) ) . '">', '</a>' ),
			10 => sprintf( __( 'Episode draft updated. %1$sPreview episode%2$s.', 'seriously-simple-podcasting' ), '<a target="_blank" href="' . esc_url( add_query_arg( 'preview', 'true', get_permalink( $post_ID ) ) ) . '">', '</a>' ),
		);

		return $messages;
	}

	/**
	 * Register columns for podcast list table
	 *
	 * @param array $defaults Default columns
	 *
	 * @return array           Modified columns
	 */
	public function register_custom_column_headings( $defaults ) {

		$columns_to_add = array();

		if ( ssp_is_connected_to_castos() ) {
			$columns_to_add['ssp_sync_status'] = __( 'Sync', 'seriously-simple-podcasting' );
		}

		$columns_to_add['ssp_cover'] = __( 'Cover', 'seriously-simple-podcasting' );

		$columns_to_add = apply_filters( 'ssp_admin_columns_episodes', $columns_to_add );

		$columns_to_unset = apply_filters( 'ssp_remove_admin_columns_episodes', array( 'comments' ) );

		// remove columns
		foreach ( $columns_to_unset as $column ) {
			if ( isset( $defaults[ $column ] ) ) {
				unset( $defaults[ $column ] );
			}
		}

		// add new columns before last default one
		$columns = array_slice( $defaults, 0, 1 ) + $columns_to_add + array_slice( $defaults, 1 );

		return $columns;
	}

	/**
	 * Display column data in podcast list table
	 *
	 * @param string  $column_name Name of current column
	 * @param integer $post_id ID of episode
	 *
	 * @return void
	 */
	public function manage_custom_columns( $column_name, $post_id ) {
		if ( 0 !== strpos( $column_name, 'ssp_' ) ) {
			return;
		}

		switch ( $column_name ) {
			case 'ssp_cover':
				$value = ssp_episode_image( $post_id, 40 );
				$value = $value ?: '<span aria-hidden="true" class="no-image"></span>';
				break;

			case 'ssp_sync_status':
				$status = $this->episode_repository->check_episode_sync_status( $post_id );
				$link   = get_edit_post_link( $post_id );
				$value  = ssp_renderer()->fetch( 'settings/sync-label', compact( 'status', 'link' ) );
				break;

			default:
				$value = '';
				break;
		}
		echo apply_filters( 'ssp_custom_column_value', $value, $column_name, $post_id );
	}

	/**
	 * Clear the cache on post save.
	 *
	 * @param int    $id POST ID
	 * @param object $post WordPress Post Object
	 *
	 * @return void
	 */
	public function invalidate_cache( $id, $post ) {

		if ( in_array( $post->post_type, ssp_post_types( true ) ) ) {
			wp_cache_delete( 'episodes', 'ssp' );
			wp_cache_delete( 'episode_ids', 'ssp' );
		}
	}
}
