<?php

namespace Barn2\Plugin\Password_Protected_Categories;

use Barn2\Plugin\Password_Protected_Categories\Dependencies\Lib\Registerable;
use Barn2\Plugin\Password_Protected_Categories\Dependencies\Lib\Service\Premium_Service;
use Barn2\Plugin\Password_Protected_Categories\Dependencies\Lib\Util;

/**
 * This class provides protection for protected categories and posts (for all post types) on the front end.
 *
 * Protection measures include removing categories (and posts belonging to these categories) from search results,
 * the main posts page, archives, navigation menus, and widgets.
 *
 * @package   Barn2\password-protected-categories
 * @author    Barn2 Plugins <support@barn2.co.uk>
 * @license   GPL-3.0
 * @copyright Barn2 Media Ltd
 */
class Term_Protector implements Premium_Service, Registerable {




	private $tax_queries         = [];
	private $hidden_term_ids     = false;
	private $unlocked_categories = false;

	/**
	 * Hooks for filtering posts, categories, nav menus, etc
	 */
	public function register() {
		// Adjust query to exclude posts in private or password protected categories
		add_action( 'pre_get_posts', [ $this, 'pre_get_posts' ] );

		// Remove private and password protected categories (and their posts) from nav menus
		add_filter( 'wp_get_nav_menu_items', [ $this, 'protect_nav_menus' ], 10, 3 );

		// Adjust query for 'get_terms' to exclude private and password protected categories
		add_filter( 'get_terms_args', [ $this, 'get_terms_args' ], 10, 2 );

		do_action( 'ppc_term_protection_hooks_registered', $this );

		add_filter( 'document_library_pro_folder_categories', [ $this, 'dlp_category_visibility' ], 10, 1 );
		add_filter( 'document_library_pro_data_column_terms', [ $this, 'dlp_category_visibility' ], 10, 1 );
		add_filter( 'posts_table_pro_data_column_terms', [ $this, 'dlp_category_visibility' ], 10, 1 );

		add_filter( 'document_library_pro_form_terms', [ $this, 'dlp_form_term_visibility' ], 10, 2 );
	}

	public function pre_get_posts( &$query ) {
		if (
			$query->is_singular() || $query->is_404() || $query->is_preview() || $query->is_robots() || $query->is_embed()
			|| $query->is_trackback() || $query->is_comment_feed() || ( is_admin() && current_user_can( 'manage_options' ) )
		) {
			return;
		}

		$set_query_vars = false;

		// If not the main query (e.g. a custom loop), or we're not in a protected taxonomy, we need to exclude posts in any hidden categories from this query.
		if ( ! $query->is_main_query() ) {
			$set_query_vars = true;
		} elseif ( ! $this->is_protectable_product_category( $query ) && $this->is_protected_tax( $query ) ) {
			$set_query_vars = true;
		} elseif ( $query->is_main_query() && is_front_page() && is_home() ) {
			$set_query_vars = true;
		} elseif ( $query->is_main_query() && $query->is_posts_page ) {
			$set_query_vars = true;
		} elseif ( $query->is_main_query() && $query->is_archive() ) {
			$set_query_vars = true;
		} elseif ( $query->is_main_query() && $query->is_search() ) {
			$set_query_vars = true;
		}

		if ( $set_query_vars ) {
			// pre_get_posts is an action (not a filter) so we don't need to return anything. $query is passed by reference.
			$query->query_vars = $this->build_tax_query( $query );
		}
	}

	private function is_protectable_product_category( $query ) {
		// If using WooCommerce Protected Categories, defer to this plugin for product categories.
		if ( Util::is_barn2_plugin_active( '\\Barn2\\Plugin\\WC_Protected_Categories\\wpc' ) && Util::is_woocommerce_active() && $query->is_tax( 'product_cat' ) ) {
			return true;
		}

		return false;
	}

	private function is_protected_tax( $query ) {
		if ( $query->is_category() || $query->is_tax( PPC_Util::get_protectable_taxonomies() ) ) {
			// Category or custom taxonomy archive
			$term = $query->get_queried_object();

			if ( ! ( $term instanceof \WP_Term ) ) {
				return false;
			}

			$term_visibility = PPC_Util::get_term_visibility( $term );

			if ( $term_visibility->is_public() ) {
				return false;
			}

			return $term_visibility->is_protected();
		}

		return false;
	}

	public function protect_nav_menus( $menu_items, $menu, $args ) {
		$removed_items          = [];
		$protectable_taxonomies = PPC_Util::get_protectable_taxonomies();

		foreach ( $menu_items as $key => $menu_item ) {
			if ( 'taxonomy' === $menu_item->type && \in_array( $menu_item->object, $protectable_taxonomies ) ) {
				if ( $term = \get_term_by( 'id', $menu_item->object_id, $menu_item->object ) ) {
					$term_visibility = PPC_Util::get_term_visibility( $term );

					if ( $term_visibility->is_hidden() && ! $term_visibility->is_unlocked() ) {
						$removed_items[] = $menu_item->ID;
						unset( $menu_items[ $key ] );
					}
				}
			} elseif ( 'post_type' === $menu_item->type ) {
				if ( PPC_Util::is_hidden_post( $menu_item->object_id ) && ! PPC_Util::is_unlocked_post( $menu_item->object_id ) ) {
					$removed_items[] = $menu_item->ID;
					unset( $menu_items[ $key ] );
				} elseif ( PPC_Util::is_hidden_post( $menu_item->object_id ) && $this->post_has_at_least_one_protected_category( $menu_item->object_id ) ) {
					$removed_items[] = $menu_item->ID;
					unset( $menu_items[ $key ] );
				}
			}
		} // foreach menu item
		// Now find and remove any children of any removed menu item
		while ( $removed_items ) {
			$child_items_removed = [];

			foreach ( $menu_items as $key => $menu_item ) {
				if ( \in_array( $menu_item->menu_item_parent, $removed_items ) ) {
					$child_items_removed[] = $menu_item->ID;
					unset( $menu_items[ $key ] );
				}
			}
			// Update the removed list with the removed child items and start over
			$removed_items = $child_items_removed;
		}

		return \array_values( $menu_items );
	}

	public function post_has_at_least_one_protected_category( $post_id ) {
		$terms = PPC_Util::get_the_term_visibility( $post_id );
		$has   = false;

		foreach ( $terms as $term_visibility ) {
			if ( $term_visibility->is_protected( true ) && ! $term_visibility->is_unlocked() ) {
				$has = true;
			}
		}

		return $has;
	}

	public function post_has_no_unlocked_protected_category( $post_id ) {
		$terms = PPC_Util::get_the_term_visibility( $post_id );
		$has   = true;

		foreach ( $terms as $term_visibility ) {
			if ( ! $term_visibility->is_protected( true ) && $term_visibility->is_unlocked() ) {
				$has = false;
			}
		}

		return $has;
	}

	public function get_terms_args( $args, $taxonomies ) {

		// Gutenberg compatibility.
		if ( is_admin() || defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return $args;
		}

		// Bail if our internal flag is set.
		if ( isset( $args['ppc_check'] ) ) {
			return $args;
		}

		/**
		 * Filter whether to bail on this get_terms() call.
		 *
		 * @param bool $should_bail Whether to bail on this get_terms() call.
		 * @param array $args The get_terms() arguments.
		 * @param array $taxonomies The taxonomies being queried.
		 * @param Term_Protector $this The Term_Protector instance.
		 * @return bool
		 */
		$should_bail = apply_filters( 'ppc_should_bail_get_terms_args', false, $args, $taxonomies, $this );

		if ( $should_bail ) {
			return $args;
		}

		// Bail if include is set - we set exclude_tree to hide protected categories, which is ignored if include is set.
		if ( ! empty( $args['include'] ) ) {
			return $args;
		}

		// Bail if we're getting the terms for one or more objects.
		if ( ! empty( $args['object_ids'] ) ) {
			return $args;
		}

		// Bail if 'get' => 'all' set (e.g. when getting the term hierarchy or get_term_by call).
		if ( 'all' === $args['get'] || 1 === $args['number'] ) {
			return $args;
		}

		// Bail if get_terms() is calling itself (it does this when 'exclude_tree' is set), to avoid an infinite loop.
		// When calling itself, 'child_of' => parent, 'fields' => 'ids' and 'hide_empty' => 0.
		if ( ! empty( $args['child_of'] ) && 'ids' === $args['fields'] && ! $args['hide_empty'] ) {
			return $args;
		}

		// Bail if we're fetching terms for taxonomies which are not protectable.
		if ( $taxonomies && ! \array_intersect( $taxonomies, PPC_Util::get_protectable_taxonomies() ) ) {
			return $args;
		}

		// Bail if there are no hidden terms.
		if ( ! ( $hidden_term_ids = $this->get_hidden_term_ids() ) ) {
			return $args;
		}

		$unlocked_categories = $this->unlocked_categories();

		// Merge in any other exclude trees.
		if ( ! empty( $args['exclude_tree'] ) ) {
			$hidden_term_ids = \array_unique( \array_merge( \wp_parse_id_list( $args['exclude_tree'] ), $hidden_term_ids ) );
		}

		if ( ! empty( $unlocked_categories ) && is_array( $unlocked_categories ) ) {
			$hidden_term_ids = array_diff( $hidden_term_ids, $unlocked_categories );
		}

		$args['exclude_tree'] = $hidden_term_ids;

		return $args;
	}

	public function get_hidden_term_ids() {
		if ( false === $this->hidden_term_ids ) {
			$this->hidden_term_ids = PPC_Util::get_hidden_terms( PPC_Util::get_protectable_taxonomies(), 'ids' );
		}
		return $this->hidden_term_ids;
	}

	public function unlocked_categories() {
		if ( false === $this->unlocked_categories ) {
			$this->unlocked_categories = [];

			// Get all the product categories, and check which are unlocked.
			$terms = PPC_Util::get_hidden_terms( PPC_Util::get_protectable_taxonomies() );

			foreach ( PPC_Util::to_term_visibilities( $terms ) as $category ) {
				if ( $category->is_unlocked() ) {
					$this->unlocked_categories[] = $category->term->term_id;
				}
			}
		}
		return $this->unlocked_categories;
	}

	public function dlp_category_visibility( $categories ) {
		// Gutenberg compatibility.
		if ( is_admin() || defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return $categories;
		}

		if ( PPC_Util::get_option( 'show_protected' ) ) {
			return $categories;
		}

		if ( ! is_array( $categories ) ) {
			return $categories;
		}

		$public_categories = [];
		foreach ( $categories as $category ) {
			if ( ! in_array( $category->term_id, $this->get_hidden_term_ids() ) || in_array( $category->term_id, $this->unlocked_categories() ) ) {
				$public_categories[] = $category;
			}
		}

		return $public_categories;
	}

	/**
	 * Filter the terms and return visible terms for displaying on the submit form
	 *
	 * @param array $terms
	 * @param string $taxonomy
	 * @return array
	 */
	public function dlp_form_term_visibility( $terms, $taxonomy ) {
		// Return if not categories term
		if ( $taxonomy !== 'doc_categories' ) {
			return $terms;
		}

		$unlocked_categories = $this->unlocked_categories();

		// Display all terms when show protected is enabled
		if ( PPC_Util::get_option( 'show_protected' ) ) {
			return $terms;
		}

		$visible_terms = [];

		foreach ( $terms as $term ) {
			$visibility = new Term_Visibility( $term );
			if ( ! $visibility->is_protected( true ) || in_array( $term->term_id, $unlocked_categories, true ) ) {
				$visible_terms[] = $term;
			}
		}

		return $visible_terms;
	}

	private function build_tax_query( $query ) {
		$query_vars = $query->query_vars;
		$taxonomies = PPC_Util::get_protectable_taxonomies();

		if ( ! empty( $query_vars['post_type'] ) ) {
			$post_type = $query_vars['post_type'];

			if ( \in_array( $post_type, [ 'revision', 'nav_menu_item' ] ) ) {
				return $query_vars;
			}
			if ( 'any' === $query_vars['post_type'] ) {
				$post_type = \get_post_types( [ 'exclude_from_search' => false ] );
			}

			$taxonomies = \array_intersect( \get_object_taxonomies( $post_type, 'names' ), $taxonomies );
		}

		if ( ! $taxonomies ) {
			// No password protectable taxonomies found, so return query vars
			return $query_vars;
		}

		$tax_query       = [];
		$taxonomies_hash = \md5( wp_json_encode( $taxonomies ) );

		// Check tax query cache first
		if ( isset( $this->tax_queries[ $taxonomies_hash ] ) ) {
			$tax_query = $this->tax_queries[ $taxonomies_hash ];
		} else {
			// Not in cache
			// If the query is for category, then make visible the specific unlocked category
			if ( $query->is_category ) {
				$unlocked_term = PPC_Util::get_unlocked_term();
				if ( $unlocked_term ) {
					$tax_query = [
						'taxonomy'         => $unlocked_term['taxonomy'],
						'field'            => 'term_id',
						'terms'            => $unlocked_term['term_id'],
						'operator'         => 'IN',
						'include_children' => true,
					];
				}
			} else {
				// If the query is something else than category
				$taxonomy_terms = [];
				$hidden_terms   = PPC_Util::get_hidden_terms( $taxonomies, 'all' );
				foreach ( $hidden_terms as $term ) {
					$vis = new Term_Visibility( $term );

					if ( $vis->is_unlocked() ) {
						continue;
					}

					if ( \array_key_exists( $term->taxonomy, $taxonomy_terms ) ) {
						$taxonomy_terms[ $term->taxonomy ][] = $term->term_id;
					} else {
						$taxonomy_terms[ $term->taxonomy ] = [ $term->term_id ];
					}
				}
				foreach ( $taxonomy_terms as $taxonomy => $term_ids ) {
					$tax_query[] = [
						'taxonomy'         => $taxonomy,
						'field'            => 'term_id',
						'terms'            => $term_ids,
						'operator'         => 'NOT IN',
						'include_children' => true,
					];
				}
			}
			if ( \count( $tax_query ) > 1 ) {
				$tax_query['relation'] = 'AND';
			}
			$this->tax_queries[ $taxonomies_hash ] = $tax_query;
		}

		if ( $tax_query ) {
			// If there's already a tax query present, wrap it our query and set as AND relation
			if ( ! empty( $query_vars['tax_query'] ) ) {
				$tax_query = [
					'relation' => 'AND',
					[ $tax_query ],
					[ $query_vars['tax_query'] ],
				];
			}

			$query_vars['tax_query'] = $tax_query;
		}

		return $query_vars;
	}
}
