<?php
/**
 * Ads Main Class
 *
 * @package AdThrive Ads
 */

namespace AdThrive_Ads\Components\Ads;

/**
 * Main class
 */
class Main {

	/**
	 * Content types to support
	 *
	 * @var array OBJECT_TYPE_POST
	 */
	const OBJECT_TYPES = array( 'page', 'post' );

	/**
	 * Add hooks
	 */
	public function setup() {
		add_action( 'init', array( $this, 'init' ) );

		add_filter( 'adthrive_ads_options', array( $this, 'add_options' ), 10, 1 );

		add_action( 'cmb2_admin_init', array( $this, 'all_objects' ) );

		add_action( 'wp_head', array( $this, 'head_scripts' ), 1 );

		add_filter( 'body_class', array( $this, 'body_class' ) );

		// 21 is higher than priority 20 set by many of our pubs themes, meaning classes will be added after the theme class-stripping occurs
		add_filter( 'body_class', array( $this, 'adthrive_cat_class' ), 21 );

		add_action( 'wp_ajax_adthrive_terms', array( $this, 'ajax_terms' ) );
	}

	/**
	 * Init hook - check for parameter to update downloaded file
	 */
	public function init() {
		if ( isset( $_GET['atSyncStatus'] ) ) {
			header( 'Content-Type: application/json; charset=utf-8' );

			$adthrive_options = get_option( 'adthrive_options' );
			$return = '';

			if ( $adthrive_options ) {
				$status_type = sanitize_text_field( wp_unslash( $_GET['atSyncStatus'] ) );
				switch ( $status_type ) {
					case 'site_js':
						// Fall through to next case
					case 'site_css':
						if ( isset( $adthrive_options[ $_GET['atSyncStatus'] ] ) ) {
							$return = array(
								'timestamp' => $adthrive_options[ $status_type ]['timestamp'],
								$status_type => $adthrive_options[ $status_type ]['content'],
							);
						} else {
							$error = "{$status_type} not set in adthrive_options";
						}
						break;
					case 'cls':
						if ( isset( $adthrive_options['cls-deployments'] ) ) {
							$return = $adthrive_options['cls-deployments']['content'];
						} else {
							$error = 'cls-deployments not set in adthrive_options';
						}
						break;
				}
			} else {
				$error = 'adthrive_options not set';
			}
			if ( isset( $error ) ) {
				print wp_json_encode( array( 'error' => $error ) );
			} else {
				print wp_json_encode( $return );
			}
			exit();
		}
	}

	/**
	 * AJAX method to get terms with the matched search query for the specified taxonomy
	 */
	public function ajax_terms() {
		if ( isset( $_GET['query'] ) ) {
			$query = sanitize_text_field( wp_unslash( $_GET['query'] ) );
		}

		if ( isset( $_GET['taxonomy'] ) ) {
			$taxonomy = sanitize_text_field( wp_unslash( $_GET['taxonomy'] ) );
		}

		wp_send_json( $this->get_term_selectize( $taxonomy, $query ) );
	}

	/**
	 * Adds classes to disable ads based on the plugin settings
	 *
	 * @param string|array $classes One or more classes to add to the class list.
	 */
	public function body_class( $classes ) {
		if ( is_singular() ) {
			global $post;

			$disable = get_post_meta( get_the_ID(), 'adthrive_ads_disable', true );
			$disable_content_ads = get_post_meta( get_the_ID(), 'adthrive_ads_disable_content_ads', true );
			$disable_auto_insert_videos = get_post_meta( get_the_ID(), 'adthrive_ads_disable_auto_insert_videos', true );
			$re_enable_ads_on = get_post_meta( get_the_ID(), 'adthrive_ads_re_enable_ads_on', true );

			$disabled_categories = \AdThrive_Ads\Options::get( 'disabled_categories' );
			$disabled_tags = \AdThrive_Ads\Options::get( 'disabled_tags' );

			$categories = get_the_category( $post->ID );
			$tags = get_the_tags( $post->ID );

			$category_names = is_array( $categories ) ? array_map( array( $this, 'pluck_name' ), $categories ) : array();
			$tag_names = is_array( $tags ) ? array_map( array( $this, 'pluck_name' ), $tags ) : array();

			if ( ! $re_enable_ads_on || false === trim( $re_enable_ads_on ) || $re_enable_ads_on > time() ) {
				if ( $disable ) {
					$classes[] = 'adthrive-disable-all';
				}

				if ( $disable_content_ads || in_array( 'noads', $tag_names, true ) ) {
					$classes[] = 'adthrive-disable-content';
				}

				if ( $disable_auto_insert_videos || in_array( 'noads', $tag_names, true ) ) {
					$classes[] = 'adthrive-disable-video';
				}
			}

			if ( is_array( $disabled_categories ) && array_intersect( $disabled_categories, $category_names ) ) {
				$classes[] = 'adthrive-disable-all';
			} elseif ( is_array( $disabled_tags ) && array_intersect( $disabled_tags, $tag_names ) ) {
				$classes[] = 'adthrive-disable-all';
			}
		} elseif ( is_404() ) {
			$classes[] = 'adthrive-disable-all';
		}

		return $classes;
	}

	/**
	 * Adds Content Specific Video Playlist category classes to the body based on the plugin settings
	 *
	 * @param string|array $classes One or more classes to add to the class list.
	 */
	public function adthrive_cat_class( $classes ) {
		if ( is_singular() ) {
			global $post;
			$categories = get_the_category( $post->ID );
			$category_names = is_array( $categories ) ? array_map( array( $this, 'pluck_name' ), $categories ) : array();

			if ( 'on' === \AdThrive_Ads\Options::get( 'content_specific_playlists' ) ) {
				$category_body_classes = is_array( $categories ) ? array_map( array( $this, 'pluck_body_class' ), $categories ) : array();
				$classes = array_merge( $classes, $category_body_classes );
			}
		}
		return $classes;
	}

	/**
	 * Gets the object name
	 *
	 * @param object $obj    An object with a name property
	 *
	 * @return string The object name
	 */
	private function pluck_name( $obj ) {
		return $obj->name;
	}

	/**
	 * Gets the object nicename
	 *
	 * @param object $obj    An object with a nicename property
	 *
	 * @return string Adthrive class name formatted for use in the body tag
	 */
	private function pluck_body_class( $obj ) {
		return 'adthrive-cat-' . $obj->category_nicename;
	}

	/**
	 * Gets just the object name property
	 *
	 * @param object $obj    An object with a name property
	 *
	 * @return string An object with just a name property
	 */
	private function get_selectize( $obj ) {
		return array(
			'text' => $obj->name,
			'value' => $obj->name,
		);
	}

	/**
	 * Add fields to the options metabox for page and posts
	 */
	public function all_objects() {
		$post_meta = new_cmb2_box(
			array(
				'id' => 'adthrive_ads_object_metabox',
				'title' => __( 'Raptive Ads', 'adthrive_ads' ),
				'object_types' => self::OBJECT_TYPES,
			)
		);

		$post_meta->add_field(
			array(
				'name' => __( 'Disable all ads', 'adthrive_ads' ),
				'id' => 'adthrive_ads_disable',
				'type' => 'checkbox',
			)
		);

		$post_meta->add_field(
			array(
				'name' => __( 'Disable content ads', 'adthrive_ads' ),
				'id' => 'adthrive_ads_disable_content_ads',
				'type' => 'checkbox',
			)
		);

		$post_meta->add_field(
			array(
				'name' => __( 'Disable auto-insert video players', 'adthrive_ads' ),
				'id' => 'adthrive_ads_disable_auto_insert_videos',
				'type' => 'checkbox',
			)
		);

		$post_meta->add_field(
			array(
				'name' => __( 'Re-enable ads on', 'adthrive_ads' ),
				'desc' => __( 'All ads on this post will be enabled on the specified date', 'adthrive_ads' ),
				'id'   => 'adthrive_ads_re_enable_ads_on',
				'type' => 'text_date_timestamp',
			)
		);

		if ( \AdThrive_Ads\Options::get( 'disable_video_metadata' ) === 'on' ) {
			$post_meta->add_field(
				array(
					'name' => __( 'Enable Video Metadata', 'adthrive_ads' ),
					'desc' => __( 'Enable adding metadata to video player on this post', 'adthrive_ads' ),
					'id'   => 'adthrive_ads_enable_metadata',
					'type' => 'checkbox',
				)
			);
		} else {
			$post_meta->add_field(
				array(
					'name' => __( 'Disable Video Metadata', 'adthrive_ads' ),
					'desc' => __( 'Disable adding metadata to video player on this post', 'adthrive_ads' ),
					'id'   => 'adthrive_ads_disable_metadata',
					'type' => 'checkbox',
				)
			);
		}

		if ( \AdThrive_Ads\Options::get( 'disable_admin_ads' ) === 'on' ) {
			$post_meta->add_field(
				array(
					'name' => __( 'Enable ads when previewing post', 'adthrive_ads' ),
					'desc' => __( 'Enable all ads when previewing a post or customizing a theme in WordPress Admin', 'adthrive_ads' ),
					'id'   => 'adthrive_ads_enable_admin_ads',
					'type' => 'checkbox',
				)
			);
		} else {
			$post_meta->add_field(
				array(
					'name' => __( 'Disable ads when previewing post', 'adthrive_ads' ),
					'desc' => __( 'Disable all ads when previewing a post or customizing a theme in WordPress Admin', 'adthrive_ads' ),
					'id'   => 'adthrive_ads_disable_admin_ads',
					'type' => 'checkbox',
				)
			);
		}
	}

	/**
	 * Add fields to the options metabox for the plugin settings.
	 *
	 * @param CMB $cmb A CMB metabox instance
	 */
	public function add_options( $cmb ) {
		$cmb->add_field(
			array(
				'name' => __( 'Site Id', 'adthrive_ads' ),
				'desc' => __( 'Add your Raptive Site ID', 'adthrive_ads' ),
				'id' => 'site_id',
				'type' => 'text',
				'attributes' => array(
					'required' => 'required',
					'pattern' => '[0-9a-f]{24}',
					'title' => 'The site id needs to match the one provided by Raptive exactly',
				),
			)
		);

		$cmb->add_field(
			array(
				'name' => 'Disabled for Categories',
				'desc' => 'Disable ads for the selected categories.',
				'id' => 'disabled_categories',
				'type' => 'text',
				'escape_cb' => array( $this, 'selectize_escape' ),
				'sanitization_cb' => array( $this, 'selectize_sanitize' ),
			)
		);

		$cmb->add_field(
			array(
				'name' => 'Disabled for Tags',
				'desc' => 'Disable ads for the selected tags.',
				'id' => 'disabled_tags',
				'type' => 'text',
				'escape_cb' => array( $this, 'selectize_escape' ),
				'sanitization_cb' => array( $this, 'selectize_sanitize' ),
			)
		);

		$cmb->add_field(
			array(
				'name' => 'Disable Video Metadata',
				'desc' => 'Disable adding metadata to video players. Caution: This is a site-wide change. Only choose if metadata is being loaded another way.',
				'id' => 'disable_video_metadata',
				'type' => 'checkbox',
			)
		);

		$cmb->add_field(
			array(
				'name' => 'CLS Optimization',
				'desc' => "Enable solution to reduce ad-related CLS
			</br>Clear your site's cache after saving this setting to apply the update across your site. Get more details on CLS optimization <a href='https://help.raptive.com/hc/en-us/articles/360048229151' target='_blank'>here.</a>",
				'id' => 'cls_optimization',
				'type' => 'checkbox',
				'default_cb' => array( $this, 'cls_checkbox_default' ),
			)
		);

		$cmb->add_field(
			array(
				'name' => 'Disable ads within WordPress Admin',
				'desc' => 'Disable all ads when previewing a post or customizing a theme in WordPress Admin. Recommended when using post or page builders.',
				'id'   => 'disable_admin_ads',
				'type' => 'checkbox',
			)
		);

		return $cmb;
	}

	/**
	 * Returns true unless the checkbox was explicitly unchecked.
	 * Note: Working with the CMB2 checkbox is odd. This function attempts to:
	 * 1. Default the checkbox to 'on'
	 * 2. If the `adthrive_ads` options are stored in the database, look for the `cls_optimization`
	 * option in they array.
	 * 3. If the `cls_optimization` option is not stored, that means a user explicitly disabled the
	 * option so don't force it to true.
	 */
	public function cls_checkbox_default( $field_args, $field ) {
		$cls_optimization = 'on';
		$adthrive_options = get_option( $field->object_id, false );
		if ( $adthrive_options ) {
			if ( isset( $adthrive_options['cls_optimization'] ) ) {
				$cls_optimization = $adthrive_options['cls_optimization'];
			} else {
				$cls_optimization = null;
			}
		}
		return isset( $cls_optimization ) ? true : false;
	}

	/**
	 * Convert a selectize field array value to string
	 *
	 * @param  mixed $value The actual field value.
	 * @return String Field value converted to a string
	 */
	public function selectize_escape( $value ) {
		if ( is_string( $value ) ) {
			return $value;
		}

		return ! empty( $value ) ? implode( ',', $value ) : null;
	}

	/**
	 * Convert a selectize field value to array
	 *
	 * @param  mixed $value The actual field value.
	 * @return array Field value converted to an array
	 */
	public function selectize_sanitize( $value ) {
		if ( is_array( $value ) ) {
			return $value;
		}

		return ! empty( $value ) ? explode( ',', $value ) : null;
	}

	/**
	 * Check if the user is within admin
	 *
	 * True if
	 * - user is editing a post/page
	 * - user is customizing a theme
	 * - user is previewing an unpublished post/page
	 * - user is viewing a published post/page while logged into WP and has higher roles than subscriber & customer
	 *
	 * @return boolean
	 */
	public function is_admin() {
		$is_admin = false;

		if ( is_admin() ) {
			// User is in the admin.
			$is_admin = true;
		} elseif ( is_customize_preview() ) {
			// User is customizing a theme.
			$is_admin = true;
		} elseif ( is_preview() ) {
			// User is previewing an unpublished post/page.
			$is_admin = true;
		} elseif ( is_user_logged_in() ) {
			// User is logged into WP. Check if user has an edit capability ( all roles beyond Subscriber ).
			if ( current_user_can( 'edit_posts' ) ) {
				$is_admin = true;
			}
		}

		return $is_admin;
	}

	/**
	 * Check if admin ads are disabled
	 *
	 * @return boolean
	 */
	public function admin_ads_disabled() {
		// Check site level settings.
		$disable_site_admin_ads = \AdThrive_Ads\Options::get( 'disable_admin_ads' ) === 'on';

		// Check page/post level settings if it's overriding site level settings.
		if ( is_singular( self::OBJECT_TYPES ) ) {
			if ( $disable_site_admin_ads ) {
				$enable_post_admin_ads = get_post_meta( get_the_ID(), 'adthrive_ads_enable_admin_ads', true ) === 'on';
				return ! $enable_post_admin_ads;
			} else {
				$disable_post_admin_ads = get_post_meta( get_the_ID(), 'adthrive_ads_disable_admin_ads', true ) === 'on';
				return $disable_post_admin_ads;
			}
		}

		return $disable_site_admin_ads;
	}

	/**
	 * Add header scripts for ads.
	 */
	public function head_scripts() {
		if ( $this->is_admin() && $this->admin_ads_disabled() ) {
			// Don't load ads if the user is in the admin and admin ads are disabled.
			return;
		}

		$this->ad_head();
		$this->adthrive_preload();
	}

	/**
	 * Add the AdThrive ads script
	 */
	private function ad_head() {
		$data['site_id'] = \AdThrive_Ads\Options::get( 'site_id' );
		$cls_optimization = \AdThrive_Ads\Options::get( 'cls_optimization' );
		// if cls_optimization is false in the plugin, cls could still be on in ads min, so we set this here
		$data['plugin_debug'] = isset( $_GET['plugin_debug'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['plugin_debug'] ) ) ? 'true' : 'false';
		$thrive_architect_enabled = isset( $_GET['tve'] ) && sanitize_key( $_GET['tve'] ) === 'true';
		if ( isset( $_SERVER['REQUEST_URI'] ) ) {
			$request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
			$widget_preview_active = strpos( $request_uri, 'wp-admin/widgets.php' ) !== false;
		} else {
			$widget_preview_active = false;
		}

		if ( isset( $data['site_id'] ) && preg_match( '/[0-9a-f]{24}/i', $data['site_id'] ) && ! $thrive_architect_enabled && ! $widget_preview_active ) {
			$body_classes = $this->body_class( array() );
			if ( 'on' === $cls_optimization ) {
				$cls_data = $this->parse_cls_deployment();
				$data = array_merge( $data, $cls_data );

				$site_ads = new \AdThrive_Ads\SiteAds();
				$data['site_js'] = $site_ads->get_site_js();
				$data['site_css'] = $this->get_option_value( 'site_css' );
				$disable_all = in_array( 'adthrive-disable-all', $body_classes, true );
				if ( ! empty( $data['site_js'] ) ) {
					$decoded_data = json_decode( $data['site_js'] );
					if ( $this->has_essential_site_ads_keys( $decoded_data ) ) {
						require 'partials/insertion-includes.php';

						add_action(
							'wp_head',
							function () use ( $data ) {
								$this->insert_cls_file( 'comscore-loader', $data );
							},
							100
						);

						add_action(
							'wp_head',
							function () use ( $data ) {
								$this->insert_cls_file( 'cls-disable-ads', $data );
							},
							100
						);

						if ( ! $disable_all && null !== $decoded_data && isset( $decoded_data->adUnits ) ) {
							$adunits = $decoded_data->adUnits;
							foreach ( $adunits as $adunit ) {
								if ( 'Header' === $adunit->location && isset( $adunit->dynamic ) ) {
									if ( ! isset( $adunit->dynamic->spacing ) ) {
										$adunit->dynamic->spacing = 0;
									}
									if ( ! isset( $adunit->dynamic->max ) ) {
										$adunit->dynamic->max = 0;
									} else {
										$adunit->dynamic->max = (int) floor( $adunit->dynamic->max );
									}
									if ( ! isset( $adunit->sequence ) ) {
										$adunit->sequence = 1;
									}
									if ( true === $adunit->dynamic->enabled && 1 === $adunit->dynamic->max && 0 === $adunit->dynamic->spacing && 1 === $adunit->sequence ) {
										add_action(
											'wp_head',
											function () use ( $data ) {
												$this->insert_cls_file( 'cls-header-insertion', $data );
											},
											101
										);
									}
								}
							}
						}

						add_action(
							'wp_footer',
							function () use ( $data ) {
								$this->insert_cls_file( 'cls-insertion', $data );
								$this->check_cls_insertion();
							},
							1
						);
					} else {
						require 'partials/sync-error.php';
					}
				}
			}

			require 'partials/ads.php';
		}
	}

	/**
	 * Adthrive Preload hook - adds dns prefetch and preconnect elements to adthrive resources
	 */
	private function adthrive_preload() {
		echo '<link rel="dns-prefetch" href="https://ads.adthrive.com/">';
		echo '<link rel="preconnect" href="https://ads.adthrive.com/">';
		echo '<link rel="preconnect" href="https://ads.adthrive.com/" crossorigin>';
	}

	/**
	 * Returns true if all expected siteAds keys exist: siteId, siteName, adOptions, breakpoints, adUnits
	 * If these keys are not present, there was an issue syncing the siteAds data, and we shouldnt load any CLS
	 */
	private function has_essential_site_ads_keys( $site_ads_object ) {
		return ( null !== $site_ads_object && isset( $site_ads_object->siteId ) && isset( $site_ads_object->siteName ) && isset( $site_ads_object->adOptions ) && isset( $site_ads_object->breakpoints ) && isset( $site_ads_object->adUnits ) );
	}

	/**
	 * Check if cls insertion script tag has been inserted to page, if not set injectedFromPlugin to false
	 */
	private function check_cls_insertion() {
		echo '<script data-no-optimize="1" data-cfasync="false">';
		echo '(function () {';
		echo 'var clsElements = document.querySelectorAll("script[id^=\'cls-\']"); window.adthriveCLS && clsElements && clsElements.length === 0 ? window.adthriveCLS.injectedFromPlugin = false : ""; ';
		echo '})();';
		echo '</script>';
	}

	/**
	 * Returns hash value specified from the url params
	 */
	public function get_remote_cls_hash() {
		return isset( $_GET['plugin_remote_cls'] ) ? sanitize_text_field( wp_unslash( $_GET['plugin_remote_cls'] ) ) : '';
	}

	/**
	 * Get cls file endpoint url for the hash. If no hash specified, then return empty string
	 */
	public function get_remote_cls_file_url( $filename ) {
		$remote_cls_hash = $this->get_remote_cls_hash();

		if ( '' !== $remote_cls_hash ) {
			return esc_url( 'https://ads.adthrive.com/builds/core/' . $remote_cls_hash . '/js/cls/' . $filename . '.min.js?ts=' . strval( time() ) );
		}
		return '';
	}

	private $cls_files_inserted = array();
	/**
	 * Inserts cls file content to script tag
	 * If debug options are enabled, makes request to remote url to fetch cls files.
	 */
	public function insert_cls_file( $filename, $data ) {
		if ( in_array( $filename, $this->cls_files_inserted, true ) ) {
			// Skip insertion when filename already inserted
			return;
		}
		array_push( $this->cls_files_inserted, $filename );

		$remote_cls_file_url = $this->get_remote_cls_file_url( $filename );
		// phpcs:disable
		if ( '' !== $remote_cls_file_url ) {
			echo "<script data-no-optimize='1' data-cfasync='false' id='" . $filename . "-remote' src='" . $remote_cls_file_url . "'></script>";
		} else {
			$cls_content = $this->get_cls_file( $filename, $data );
			if ( '' !== $cls_content['branch'] ) {
				echo "<script data-no-optimize='1' data-cfasync='false' id='" . $filename . "-" . $cls_content['branch'] . "'>";
				echo $cls_content['content'];
				echo "</script>";
			}
		}
		// phpcs:enable
	}

	/**
	 * Get cls insertion file for the hash, if file for the hash is not found, return stable version
	 */
	public function get_cls_file( $filename, $data ) {
		if ( isset( $data['cls_branch'] ) ) {
			if ( isset( $data['cls_bucket'] ) && 'prod' !== $data['cls_bucket'] ) {
				$option_content = $this->get_option_value( $filename . '.' . $data['cls_branch'] );
				if ( $option_content ) {
					return array(
						'branch' => $data['cls_branch'],
						'bucket' => $data['cls_bucket'],
						'content' => $option_content,
					);
				}
			}

			$stable_option_content = $this->get_option_value( $filename . '.stable' );
			if ( $stable_option_content ) {
				return array(
					'branch' => $data['cls_branch'],
					'bucket' => $data['cls_bucket'],
					'content' => $stable_option_content,
				);
			}
		}

		return array(
			'branch' => '',
			'bucket' => '',
			'content' => '',
		);
	}

	/**
	 * Parser CLS deployment file from file system and return deployment info
	 */
	private function parse_cls_deployment() {
		$output = array();

		$cls_deployment = $this->get_option_value( 'cls-deployments' );
		if ( $cls_deployment ) {
			$output['cls_branch'] = $cls_deployment['stable'];
			$output['cls_bucket'] = 'prod';

			if ( isset( $cls_deployment['test'] ) ) {
				$output['cls_branch'] = $cls_deployment['test'];
				$output['cls_bucket'] = 'feature';
			}

			$cls_hash = $this->get_remote_cls_hash();
			if ( strlen( $cls_hash ) > 0 ) {
				$output['cls_branch'] = $cls_hash;
				$output['cls_bucket'] = 'debug';
			}
		}
		return $output;
	}

	/**
	 * Gets terms and displays them as options
	 *
	 * @param  String $taxonomy Taxonomy terms to retrieve. Default is category.
	 * @param  String $search Optional. get_terms optional arguments
	 *
	 * @return array An array of options that matches the CMB2 options array
	 */
	public function get_term_selectize( $taxonomy = 'category', $search = '' ) {
		$terms = (array) get_terms(
			array(
				'taxonomy' => $taxonomy,
				'search' => $search,
				'number' => 100,
			)
		);

		return is_array( $terms ) ? array_map( array( $this, 'get_selectize' ), $terms ) : array();
	}

	/**
	 * Get the Adthrive option value from WP transient or option storage
	 */
	private function get_option_value( $option_name ) {
		$adthrive_options = get_option( 'adthrive_options' );

		if ( false === $adthrive_options ) {
			return false;
		}

		if ( isset( $adthrive_options[ $option_name ]['content'] ) ) {
			return $adthrive_options[ $option_name ]['content'];
		}

		return false;
	}
}
