<?php
/**
 * ZoomRepository class.
 *
 * @since 2.5.19
 *
 * @package Masteriyo\Addons\Zoom
 */

namespace Masteriyo\Addons\Zoom\Repository;

defined( 'ABSPATH' ) || exit;


use Masteriyo\Addons\Zoom\Enums\ZoomMeetingStatus;
use Masteriyo\Addons\Zoom\Models\Setting;
use Masteriyo\Addons\Zoom\Models\Zoom;
use Masteriyo\Database\Model;
use Masteriyo\DateTime;
use Masteriyo\Enums\PostStatus;
use Masteriyo\Exceptions\RestException;
use Masteriyo\PostType\PostType;
use Masteriyo\Repository\AbstractRepository;
use Masteriyo\Repository\RepositoryInterface;
use ThemeGrill\Zoom\Meeting;
use ThemeGrill\Zoom\Request;
use ThemeGrill\Zoom\ServerToServerAuth;

/**
 * ZoomRepository class.
 *
 * @since 2.5.19
 */
class ZoomRepository extends AbstractRepository implements RepositoryInterface {

	/**
	 * Data stored in meta keys, but not considered "meta".
	 *
	 * @since 2.5.19
	 * @var array
	*/
	protected $internal_meta_keys = array(
		'duration'           => '_duration',
		'join_before_host'   => '_join_before_host',
		'focus_mode'         => '_focus_mode',
		'mute_upon_entry'    => '_mute_upon_entry',
		'participant_video'  => '_participant_video',
		'record'             => '_record',
		'close_registration' => '_close_registration',
		'course_id'          => '_course_id',
		'meeting_id'         => '_meeting_id',
		'start_url'          => '_start_url',
		'join_url'           => '_join_url',
		'time_zone'          => '_time_zone',
		'expires_at'         => '_expires_at',
		'starts_at'          => '_starts_at',
	);

	/**
	* Create a zoom meeting  in the database.
	*
	* @since 2.5.19
	*
	* @param \Masteriyo\Addons\Zoom\Models\Zoom $zoom Zoom object.
	*/
	public function create( Model &$zoom ) {
		if ( ! $zoom->get_created_at( 'edit' ) ) {
			$zoom->set_created_at( time() );
		}

		if ( ! $zoom->get_starts_at( 'edit' ) ) {
			$zoom->set_starts_at( time() );
		}

		// Author of the zoom should be same as that of course, because zooms are children of courses.
		if ( $zoom->get_course_id() ) {
			$zoom->set_author_id( masteriyo_get_course_author_id( $zoom->get_course_id() ) );
		}

		// Set the author of the zoom to the current user id, if the zoom doesn't have a author.
		if ( empty( $zoom->get_author_id() ) ) {
			$zoom->set_author_id( get_current_user_id() );
		}

		$zoom_return = $this->create_zoom_meeting( $zoom );

		$zoom->set_meeting_id( $zoom_return->id );
		$zoom->set_join_url( $zoom_return->join_url );
		$zoom->set_start_url( $zoom_return->start_url );

		$id = wp_insert_post(
			/**
			 * Filters new zoom data before creating.
			 *
			 * @since 2.5.19
			 *
			 * @param array $data New Zoom session data.
			 * @param Masteriyo\Addons\Zoom\Models\Zoom $zoom Zoom object.
			 */
			apply_filters(
				'masteriyo_new_zoom_data',
				array(
					'post_type'      => PostType::ZOOM,
					'post_status'    => $zoom->get_status() ? $zoom->get_status() : ZoomMeetingStatus::UPCOMING,
					'post_author'    => $zoom->get_author_id( 'edit' ),
					'post_title'     => $zoom->get_name() ? $zoom->get_name() : __( 'Zoom Meeting', 'learning-management-system' ),
					'post_content'   => $zoom->get_description(),
					'post_parent'    => $zoom->get_parent_id(),
					'comment_status' => 'closed',
					'ping_status'    => 'closed',
					'menu_order'     => $zoom->get_menu_order(),
					'post_password'  => $zoom->get_password(),
					'post_date'      => gmdate( 'Y-m-d H:i:s', $zoom->get_created_at( 'edit' )->getOffsetTimestamp() ),
					'post_date_gmt'  => gmdate( 'Y-m-d H:i:s', $zoom->get_created_at( 'edit' )->getTimestamp() ),
				),
				$zoom
			)
		);

		if ( $id && ! is_wp_error( $id ) ) {
			$zoom->set_id( $id );
			$this->update_post_meta( $zoom, true );
			// TODO Invalidate caches.

			$zoom->save_meta_data();
			$zoom->apply_changes();

			/**
			 * Fires after creating a zoom session.
			 *
			 * @since 2.5.19
			 *
			 * @param integer $id The zoom ID.
			 * @param \Masteriyo\Addons\zoom\Models\zoom $object The zoom object.
			 */
			do_action( 'masteriyo_new_zoom', $id, $zoom );
		}
	}

	/**
	 * Delete a zoom meeting from zoom server.
	 *
	 * @since 2.5.19
	 *
	 * @param $meeting_id meeting id of zoom server
	*/
	public function delete_zoom_meeting( $meeting_id ) {
		$meetings       = new Meeting( new Request() );
		$headers        = $this->get_headers();
		$delete_meeting = $meetings->delete( '/meetings/', 'DELETE', $headers, $meeting_id );
		return $delete_meeting;
	}

	/**
	 * Create a zoom meeting to the zoom server.
	 *
	 * @since 2.5.19
	 *
	 * @param \Masteriyo\Addons\Zoom\Models\Zoom $zoom Zoom object.
	 * @param string  $context Request context.
	 * Options: 'view' and 'edit'.
	 *
	 * @return object
	*/
	public function create_zoom_meeting( $zoom ) {
		$data    = $this->get_zoom_payload( $zoom );
		$headers = $this->get_headers();

		$meetings    = new Meeting( new Request() );
		$add_meeting = $meetings->create( '/users/me/meetings', 'POST', wp_json_encode( $data ), $headers );

		if ( ! empty( $add_meeting->code ) ) {
			throw new RestException( $add_meeting->code, $add_meeting->message );
		}

		return $add_meeting;
	}

	/**
	 * Return zoom payload to be sent to server.
	 *
	 * @since 2.5.19
	 *
	 * @param \Masteriyo\Addons\Zoom\Models\Zoom $zoom Zoom object.
	 *
	 * @return array
	*/
	protected function get_zoom_payload( $zoom ) {
		return array(
			'topic'      => $zoom->get_name( 'edit' ),
			'type'       => $zoom->get_type( 'edit' ),
			'start_time' => masteriyo_rest_prepare_date_response( $zoom->get_starts_at() ),
			'duration'   => $zoom->get_duration( 'edit' ),
			'password'   => $zoom->get_password( $zoom, 'edit' ),
			'timezone'   => $zoom->get_time_zone( $zoom, 'edit' ),
			'agenda'     => $zoom->get_description( 'edit' ),
			'settings'   => array(
				'host_video'        => $zoom->get_host_video( 'edit' ),
				'participant_video' => $zoom->get_participant_video( 'edit' ),
				'cn_meeting'        => false,
				'in_meeting'        => false,
				'join_before_host'  => $zoom->get_join_before_host( 'edit' ),
				'mute_upon_entry'   => $zoom->get_mute_upon_entry( 'edit' ),
				'watermark'         => false,
				'use_pmi'           => false,
			),
		);
	}

	/**
	 * Update zoom meeting details to the zoom server.
	 *
	 * @since 2.5.19
	 *
	 * @param \Masteriyo\Addons\Zoom\Models\Zoom $zoom Zoom object.
	*/
	public function update_zoom_meeting( $zoom ) {
		try {
			if ( empty( $zoom->get_meeting_id() ) ) {
				return;
			}

			$data           = $this->get_zoom_payload( $zoom );
			$headers        = $this->get_headers();
			$meetings       = new Meeting( new Request() );
			$meeting_update = $meetings->update( '/meetings/', 'PATCH', wp_json_encode( $data ), $headers, $zoom->get_meeting_id() );

			if ( ! empty( $meeting_update->code ) ) {
				throw new RestException( $meeting_update->code, $meeting_update->message );
			}

			return $meeting_update;
		} catch ( \Exception $e ) {
			return null;
		}
	}

	/**
	 * Get headers for zoom server auth authentication.
	 *
	 * @since 2.5.19
	*/
	protected function get_headers() {
		$auth    = new ServerToServerAuth( new Request() );
		$setting = new Setting();

		$account_id    = $setting->get( 'account_id' );
		$client_id     = $setting->get( 'client_id' );
		$client_secret = $setting->get( 'client_secret' );

		$auth->set_credentials( $client_id, $client_secret, $account_id );

		$token = $auth->get_token();

		if ( ! empty( $token->error ) ) {
			throw new RestException( $token->error, $token->reason );
		}

		return array(
			'Content-Type'  => 'application/json',
			'Authorization' => 'Bearer ' . $token->access_token,
		);
	}

	/**
	 * Read a zoom session.
	 *
	 * @since 2.5.19
	 *
	 * @param \Masteriyo\Addons\zoom\Models\zoom $zoom zoom object.
	 * @throws \Exception If invalid zoom.
	 */
	public function read( Model &$zoom ) {
		$zoom_post = get_post( $zoom->get_id() );

		if ( ! $zoom->get_id() || ! $zoom_post || PostType::ZOOM !== $zoom_post->post_type ) {
			throw new \Exception( __( 'Invalid zoom.', 'learning-management-system' ) );
		}

		$zoom->set_props(
			array(
				'name'        => $zoom_post->post_title,
				'created_at'  => $this->string_to_timestamp( $zoom_post->post_date_gmt ),
				'modified_at' => $this->string_to_timestamp( $zoom_post->post_modified_gmt ),
				'description' => $zoom_post->post_content,
				'parent_id'   => $zoom_post->post_parent,
				'menu_order'  => $zoom_post->menu_order,
				'status'      => $zoom_post->post_status,
				'author_id'   => $zoom_post->post_author,
				'password'    => $zoom_post->post_password,
			)
		);

		$this->read_zoom_data( $zoom );
		$this->read_extra_data( $zoom );
		$zoom->set_object_read( true );

		/**
		 * Fires after reading a zoom from database.
		 *
		 * @since 2.5.19
		 *
		 * @param integer $id The zoom ID.
		 * @param \Masteriyo\Addons\zoom\Models\zoom $object The zoom object.
		 */
		do_action( 'masteriyo_zoom_read', $zoom->get_id(), $zoom );
	}

	/**
	 * Read zoom data. Can be overridden by child classes to load other props.
	 *
	 * @since 2.5.19
	 *
	 * @param \Masteriyo\Addons\Zoom\Models\Zoom $zoom Zoom object.
	 */
	protected function read_zoom_data( &$zoom ) {
		$meta_values = $this->read_meta( $zoom );

		$set_props = array();

		$meta_values = array_reduce(
			$meta_values,
			function( $result, $meta_value ) {
				$result[ $meta_value->key ][] = $meta_value->value;
				return $result;
			},
			array()
		);

		foreach ( $this->internal_meta_keys as $prop => $meta_key ) {
			$meta_value         = isset( $meta_values[ $meta_key ][0] ) ? $meta_values[ $meta_key ][0] : null;
			$set_props[ $prop ] = maybe_unserialize( $meta_value ); // get_post_meta only unserializes single values.
		}

		$zoom->set_props( $set_props );
	}

	/**
	 * Read extra data associated with the zoom session, like button text or zoom URL.
	 *
	 * @since 2.5.19
	 *
	 * @param \Masteriyo\Addons\Zoom\Models\Zoom $zoom Zoom object.
	 */
	protected function read_extra_data( &$zoom ) {
		$meta_values = $this->read_meta( $zoom );

		foreach ( $zoom->get_extra_data_keys() as $key ) {
			$function = 'set_' . $key;

			if ( is_callable( array( $zoom, $function ) ) && isset( $meta_values[ '_' . $key ] ) ) {
				$zoom->{$function}( $meta_values[ '_' . $key ] );
			}
		}
	}

	/**
	 * Update a zoom in the database.
	 *
	 * @since 2.5.19
	 *
	 * @param \Masteriyo\Addons\Zoom\Models\zoom $zoom zoom object.
	 *
	 * @return void
	 */
	public function update( Model &$zoom ) {
		$changes = $zoom->get_changes();

		$post_data_keys = array(
			'name',
			'status',
			'parent_id',
			'menu_order',
			'description',
			'created_at',
			'modified_at',
			'slug',
			'parent_id',
			'author_id',
			'password',
		);

		$this->update_zoom_meeting( $zoom );

		// Only update the post when the post data changes.
		if ( array_intersect( $post_data_keys, array_keys( $changes ) ) ) {
			$post_data = array(
				'post_title'     => $zoom->get_name( 'edit' ),
				'post_content'   => $zoom->get_description( 'edit' ),
				'post_parent'    => $zoom->get_parent_id( 'edit' ),
				'comment_status' => 'closed',
				'post_status'    => $zoom->get_status( 'edit' ) ? $zoom->get_status( 'edit' ) : ZoomMeetingStatus::UPCOMING,
				'menu_order'     => $zoom->get_menu_order( 'edit' ),
				'post_type'      => PostType::ZOOM,
				'post_password'  => $zoom->get_password(),
				'post_author'    => $zoom->get_author_id( 'edit' ),
				'post_date'      => gmdate( 'Y-m-d H:i:s', $zoom->get_created_at( 'edit' )->getOffsetTimestamp() ),
				'post_date_gmt'  => gmdate( 'Y-m-d H:i:s', $zoom->get_created_at( 'edit' )->getTimestamp() ),
			);

			/**
			 * When updating this object, to prevent infinite loops, use $wpdb
			 * to update data, since wp_update_post spawns more calls to the
			 * save_post action.
			 *
			 * This ensures hooks are fired by either WP itself (admin screen save),
			 * or an update purely from CRUD.
			 */
			if ( doing_action( 'save_post' ) ) {
				// TODO Abstract the $wpdb WordPress class.
				$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $zoom->get_id() ) );
				clean_post_cache( $zoom->get_id() );
			} else {
				wp_update_post( array_merge( array( 'ID' => $zoom->get_id() ), $post_data ) );
			}
			$zoom->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
		} else { // Only update post modified time to record this save event.
			$GLOBALS['wpdb']->update(
				$GLOBALS['wpdb']->posts,
				array(
					'post_modified'     => current_time( 'mysql' ),
					'post_modified_gmt' => current_time( 'mysql', true ),
				),
				array(
					'ID' => $zoom->get_id(),
				)
			);
			clean_post_cache( $zoom->get_id() );
		}

		$this->update_post_meta( $zoom );

		$zoom->apply_changes();

		/* *
		 * Fires after updating a zoom.
		 *
		 * @since 2.5.19
		 *
		 * @param integer $id The zoom ID.
		 * @param \Masteriyo\Addons\zoom\Models\zoom $object The zoom object.
		 */
		do_action( 'masteriyo_update_zoom', $zoom->get_id(), $zoom );
	}

	/**
	 * Delete a zoom from the database.
	 *
	 * @since 2.5.19
	 *
	 * @param \Masteriyo\Addons\zoom\Models\zoom $zoom zoom object.
	 * @param array $args   Array of args to pass.alert-danger.
	 */
	public function delete( Model &$zoom, $args = array() ) {
		$id          = $zoom->get_id();
		$object_type = $zoom->get_object_type();

		$args = array_merge(
			array(
				'force_delete' => false,
			),
			$args
		);

		if ( ! $id ) {
			return;
		}

		if ( $args['force_delete'] ) {
			$this->delete_zoom_meeting( $zoom->get_meeting_id() );

			/**
			 * Fires before deleting a zoom.
			 *
			 * @since 2.5.19
			 *
			 * @param integer $id The zoom ID.
			 * @param \Masteriyo\Addons\Zoom\Models\Zoom $object The zoom object.
			 */
			do_action( 'masteriyo_before_delete_' . $object_type, $id, $zoom );

			wp_delete_post( $id, true );
			$zoom->set_id( 0 );

			/**
			 * Fires after deleting a zoom.
			 *
			 * @since 2.5.19
			 *
			 * @param integer $id The zoom ID.
			 * @param \Masteriyo\Addons\Zoom\Models\Zoom $object The zoom object.
			 */
			do_action( 'masteriyo_after_delete_' . $object_type, $id, $zoom );
		} else {
			/**
			 * Fires before moving a zoom to trash.
			 *
			 * @since 2.5.19
			 *
			 * @param integer $id The zoom ID.
			 * @param \Masteriyo\Addons\Zoom\Models\Zoom $object The zoom object.
			 */
			do_action( 'masteriyo_before_trash_' . $object_type, $id, $zoom );

			wp_trash_post( $id );
			$zoom->set_status( PostStatus::TRASH );

			/**
			 * Fires after moving a zoom to trash.
			 *
			 * @since 2.5.19
			 *
			 * @param integer $id The zoom ID.
			 * @param \Masteriyo\Addons\Zoom\Models\Zoom $object The zoom object.
			 */
			do_action( 'masteriyo_after_trash_' . $object_type, $id, $zoom );
		}
	}

	/**
	 * Fetch zooms.
	 *
	 * @since 2.6.5
	 *
	 * @param array $query_vars Query vars.
	 * @return \Masteriyo\Addons\Zoom\Models\Zoom[]
	 */
	public function query( $query_vars ) {
		$args = $this->get_wp_query_args( $query_vars );

		if ( ! empty( $args['errors'] ) ) {
			$query = (object) array(
				'posts'         => array(),
				'found_posts'   => 0,
				'max_num_pages' => 0,
			);
		} else {
			$query = new \WP_Query( $args );
		}

		if ( isset( $query_vars['return'] ) && 'objects' === $query_vars['return'] && ! empty( $query->posts ) ) {
			// Prime caches before grabbing objects.
			update_post_caches( $query->posts, array( PostType::ZOOM ) );
		}

		$zooms = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'masteriyo_get_zoom', $query->posts ) );

		if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) {
			return (object) array(
				'zooms'         => $zooms,
				'total'         => $query->found_posts,
				'max_num_pages' => $query->max_num_pages,
			);
		}

		return $zooms;
	}

	/**
	 * Get valid WP_Query args from a ZoomQuery's query variables.
	 *
	 * @since 2.6.5
	 *
	 * @param array $query_vars Query vars from a ZoomQuery.
	 *
	 * @return array
	 */
	protected function get_wp_query_args( $query_vars ) {
		// Map query vars to ones that get_wp_query_args or WP_Query recognize.
		$key_mapping = array(
			'status'    => 'post_status',
			'page'      => 'paged',
			'parent_id' => 'post_parent',
		);

		foreach ( $key_mapping as $query_key => $db_key ) {
			if ( isset( $query_vars[ $query_key ] ) ) {
				$query_vars[ $db_key ] = $query_vars[ $query_key ];
				unset( $query_vars[ $query_key ] );
			}
		}

		$query_vars['post_type'] = PostType::ZOOM;

		$wp_query_args = parent::get_wp_query_args( $query_vars );

		if ( ! isset( $wp_query_args['date_query'] ) ) {
			$wp_query_args['date_query'] = array();
		}
		if ( ! isset( $wp_query_args['meta_query'] ) ) {
			$wp_query_args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
		}

		// Handle date queries.
		$date_queries = array(
			'created_at'  => 'post_date',
			'modified_at' => 'post_modified',
		);

		if ( ! empty( $wp_query_args['course'] ) ) {
			$wp_query_args['meta_query'][] = array(
				array(
					'key'     => '_course_id',
					'value'   => $wp_query_args['course'],
					'compare' => 'IN',
				),
			);
		}

		foreach ( $date_queries as $query_var_key => $db_key ) {
			if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) {

				// Remove any existing meta queries for the same keys to prevent conflicts.
				$existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true );
				foreach ( $existing_queries as $query_index => $query_contents ) {
					unset( $wp_query_args['meta_query'][ $query_index ] );
				}

				$wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args );
			}
		}

		// Handle paginate.
		if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) {
			$wp_query_args['no_found_rows'] = true;
		}

		// Handle orderby.
		if ( isset( $query_vars['orderby'] ) && 'include' === $query_vars['orderby'] ) {
			$wp_query_args['orderby'] = 'post__in';
		}

		/**
		 * Filters WP Query args for zoom post type query.
		 *
		 * @since 2.6.5
		 *
		 * @param array $wp_query_args WP Query args.
		 * @param array $query_vars Query vars.
		 * @param \Masteriyo\Repository\ZoomRepository $repository Zoom repository object.
		 */
		return apply_filters( 'masteriyo_zoom_wp_query_args', $wp_query_args, $query_vars, $this );
	}
}
