<?php
/**
 * @package The7
 */

defined( 'ABSPATH' ) || exit;

class The7_Content_Importer extends WP_Import {

	const THE7_PROCESSED_DATA_KEY      = 'the7_demo_content_processed_data';
	const THE7_POST_RESOURCES_META_KEY = 'the7_exporter_post_resources';

	protected $total_posts = 0;
	protected $parsed_file = null;
	protected $filtered_post_id;
	protected $filter_by_url;
	protected $filter_by_id;

	public function import_batch( $file, $batch, $fetch_attachments = true ) {
		$this->fetch_attachments = $fetch_attachments;

		// Allow to import various danger files, assuming that the data from the demo content is safe.
		add_filter(
			'upload_mimes',
			function ( $mimes ) {
				$mimes['svg']   = 'image/svg+xml';
				$mimes['ttf']   = 'application/x-font-ttf';
				$mimes['eot']   = 'application/vnd.ms-fontobject';
				$mimes['woff']  = 'application/x-font-woff';
				$mimes['woff2'] = 'font/woff2';
				$mimes['json']  = 'application/json';
				$mimes['css']   = 'text/css';

				return $mimes;
			}
		);

		ob_start();

		if ( ! $batch ) {
			ob_end_clean();

			$this->import( $file );

			return [
				'imported' => count( $this->processed_posts ),
				'left'     => 0,
				'total'    => $this->total_posts,
			];
		}

		$this->read_processed_data_from_cache();

		add_filter( 'import_post_meta_key', array( $this, 'is_valid_meta_key' ) );
		add_filter( 'http_request_timeout', array( $this, 'bump_request_timeout' ) );

		/**
		 * Set the operation timelimit based on php `max_execution_time`, between 1 and 45 seconds.
		 *
		 * Nginx and Apache has 60 seconds default timelimit, and we used it as a top baundary (minus 20 secons for safety).
		 * By default php has 30 seconds execution time, 20 seconds is the safe default.
		 */
		$max_execution_time = 20;
		if ( function_exists( 'ini_get' ) ) {
			$max_execution_time = (int) ini_get( 'max_execution_time' ) - 10;
			$max_execution_time = $max_execution_time > 0 ? $max_execution_time : 20;
			$max_execution_time = min( $max_execution_time, 40 );
		}
		$this->set_timelimit( $max_execution_time );

		$this->import_start( $file );
		$this->cut_batch( $batch );

		$this->get_author_mapping();

		wp_suspend_cache_invalidation( true );
		$this->process_categories();
		$this->process_tags();
		$this->process_terms();
		$this->process_posts();
		wp_suspend_cache_invalidation( false );

		// Update incorrect/missing information in the DB.
		$this->backfill_parents();
		$this->backfill_attachment_urls();
		$this->remap_featured_images();

		$this->import_end();

		$this->cache_processed_data();

		$new_log = ob_get_clean();

		$this->log_add( $new_log );

		$time = time() - $this->start_time;
		$this->log_add( 'Operation took ' . $time . ' seconds.' );

		return [
			'imported' => $this->imported_posts,
			'left'     => $this->total_posts - $this->imported_posts,
			'total'    => $this->total_posts,
		];
	}

	/**
	 * The main controller for the actual import stage.
	 *
	 * @param string $file Path to the WXR file for importing
	 */
	function import( $file ) {
		add_filter(
			'wp_insert_term_data',
			function ( $data, $taxonomy, $args ) {
				if ( isset( $args['term_id'] ) ) {
					$data['term_id'] = $args['term_id'];
				}

				return $data;
			},
			10,
			3
		);

		ob_start();

		$this->set_timelimit( 9999 );

		parent::import( $file );
		$log = ob_get_clean();

		$this->log_add( $log );
	}

	protected function cut_batch( $batch ) {
		$processed_posts_ids = array_keys( $this->processed_posts );

		foreach ( $this->posts as $i => $post ) {
			if ( in_array( $post['post_id'], $processed_posts_ids, false ) ) {
				$this->imported_posts++;
				unset( $this->posts[ $i ] );
			}
		}

		$this->posts = array_slice( $this->posts, 0, $batch );
	}

	public function import_start( $file ) {
		parent::import_start( $file );

		foreach ( $this->posts as $index => $post ) {
			$only_attachments = $this->fetch_attachments && $post['post_type'] !== 'attachment';
			$only_posts       = ! $this->fetch_attachments && $post['post_type'] === 'attachment';

			if ( $only_attachments || $only_posts ) {
				unset( $this->posts[ $index ] );
			}
		}

		$this->total_posts = count( $this->posts );

		add_filter( 'wp_import_post_data_raw', [ $this, 'wc_products_filter' ] );
		add_filter(
			'import_post_meta_key',
			function( $key ) {
				return $key === static::THE7_POST_RESOURCES_META_KEY ? false : $key;
			}
		);
		add_filter(
			'wp_import_existing_post',
			function( $post_exists, $post ) {
				if ( $post['post_type'] === 'attachment' && ! empty( $post['postmeta'] ) ) {
					// Double check existed attachments.
					$flat_postmeta = wp_list_pluck( $post['postmeta'], 'value', 'key' );
					if ( isset( $flat_postmeta['_wp_attached_file'] ) ) {
						return $this->attachment_exists( $flat_postmeta['_wp_attached_file'] );
					}
				} elseif (
					$this->filtered_post_id
					&&
					(
					$post['post_id'] === $this->filtered_post_id
					||
					$post['post_type'] === 'elementor_library'
					)
				) {
					// Force to import Elementor library and requested post.
					return 0;
				}

				return $post_exists;
			},
			10,
			2
		);
	}

	public function import_end() {
		wp_cache_flush();
		foreach ( get_taxonomies() as $tax ) {
			delete_option( "{$tax}_children" );
			_get_term_hierarchy( $tax );
		}

		wp_defer_term_counting( false );
		wp_defer_comment_counting( false );

		do_action( 'import_end', $this );
	}

	/**
	 * Parse a WXR file
	 *
	 * @param string $file Path to WXR file for parsing
	 *
	 * @return array Information gathered from the WXR file
	 */
	public function parse( $file ) {
		if ( $this->parsed_file === null ) {
			$this->parsed_file = apply_filters( 'wp_import_parse', parent::parse( $file ) );
		}

		return $this->parsed_file;
	}

	/**
	 * Method override.
	 *
	 * Add custom meta to menu items.
	 *
	 * @param array $item
	 */
	public function process_menu_item( $item ) {

		// skip draft, orphaned menu items
		if ( 'draft' === $item['status'] ) {
			printf( "Skip draft menu item: %s\n", (int) $item['post_id'] );

			return;
		}

		$menu_slug = false;
		if ( isset( $item['terms'] ) ) {
			// loop through terms, assume first nav_menu term is correct menu
			foreach ( $item['terms'] as $term ) {
				if ( 'nav_menu' === $term['domain'] ) {
					$menu_slug = $term['slug'];
					break;
				}
			}
		}

		// no nav_menu term associated with this menu item
		if ( ! $menu_slug ) {
			echo "Menu item skipped due to missing menu slug:\n";
			var_dump( $item['terms'] );

			return;
		}

		$menu_id = term_exists( $menu_slug, 'nav_menu' );
		if ( ! $menu_id ) {
			printf( "Menu item skipped due to invalid menu slug: '%s' - %s\n", esc_html( $menu_slug ), (int) $item['post_id'] );

			return;
		}

		$menu_id = is_array( $menu_id ) ? $menu_id['term_id'] : $menu_id;

		foreach ( $item['postmeta'] as $meta ) {
			${$meta['key']} = $meta['value'];
		}

		if ( 'taxonomy' === $_menu_item_type && isset( $this->processed_terms[ intval( $_menu_item_object_id ) ] ) ) {
			$_menu_item_object_id = $this->processed_terms[ intval( $_menu_item_object_id ) ];
		} elseif ( 'post_type' === $_menu_item_type && isset( $this->processed_posts[ intval( $_menu_item_object_id ) ] ) ) {
			$_menu_item_object_id = $this->processed_posts[ intval( $_menu_item_object_id ) ];
		} elseif ( 'custom' !== $_menu_item_type ) {
			// associated object is missing or not imported yet, we'll retry later
			$this->missing_menu_items[] = $item;

			printf( "Missing menu item: %s\n", (int) $item['post_id'] );

			return;
		}

		if ( isset( $this->processed_menu_items[ (int) $_menu_item_menu_item_parent ] ) ) {
			$_menu_item_menu_item_parent = $this->processed_menu_items[ (int) $_menu_item_menu_item_parent ];
		} elseif ( $_menu_item_menu_item_parent ) {
			$this->menu_item_orphans[ (int) $item['post_id'] ] = (int) $_menu_item_menu_item_parent;
			$_menu_item_menu_item_parent                       = 0;
		}

		// wp_update_nav_menu_item expects CSS classes as a space separated string
		$_menu_item_classes = maybe_unserialize( $_menu_item_classes );
		if ( is_array( $_menu_item_classes ) ) {
			$_menu_item_classes = implode( ' ', $_menu_item_classes );
		}

		$args = array(
			'menu-item-object-id'   => $_menu_item_object_id,
			'menu-item-object'      => $_menu_item_object,
			'menu-item-parent-id'   => $_menu_item_menu_item_parent,
			'menu-item-position'    => (int) $item['menu_order'],
			'menu-item-type'        => $_menu_item_type,
			'menu-item-title'       => $item['post_title'],
			'menu-item-url'         => $_menu_item_url,
			'menu-item-description' => $item['post_content'],
			'menu-item-attr-title'  => $item['post_excerpt'],
			'menu-item-target'      => $_menu_item_target,
			'menu-item-classes'     => $_menu_item_classes,
			'menu-item-xfn'         => $_menu_item_xfn,
			'menu-item-status'      => $item['status'],
		);

		/**
		 * Filter menu item args.
		 *
		 * @since 7.4.1
		 */
		$args = apply_filters( 'wxr_menu_item_args', $args, $menu_id );

		$id = wp_update_nav_menu_item( $menu_id, 0, $args );
		if ( $id && ! is_wp_error( $id ) ) {
			$this->processed_menu_items[ (int) $item['post_id'] ] = (int) $id;

			printf( "Importing menu item: %s - %s\n", (int) $id, esc_html( $item['post_title'] ) );

			// Add custom meta.
			$meta_to_exclude   = array_keys( $args );
			$meta_to_exclude[] = 'menu-item-menu-item-parent';
			foreach ( $item['postmeta'] as $meta ) {
				$key = str_replace( '_', '-', ltrim( $meta['key'], '_' ) );
				if ( ! empty( $meta['value'] ) && ! in_array( $key, $meta_to_exclude, true ) ) {
					update_post_meta( $id, $meta['key'], maybe_unserialize( $meta['value'] ) );
				}
			}
		} else {
			echo "Failed to import menu item:\n";

			if ( is_wp_error( $id ) ) {
				print_r( $id->get_error_messages() );
			}

			var_dump( $item );
		}
	}

	/**
	 * @param array $raw_post
	 *
	 * @return array
	 */
	public function wc_products_filter( $raw_post ) {
		global $wpdb;

		if ( $raw_post['post_type'] !== 'product' ) {
			return $raw_post;
		}

		if ( empty( $raw_post['terms'] ) ) {
			return $raw_post;
		}

		foreach ( $raw_post['terms'] as $term ) {
			$domain = $term['domain'];

			if ( false === strpos( $domain, 'pa_' ) || taxonomy_exists( $domain ) ) {
				continue;
			}

			// Make sure taxonomy exists!
			$nicename     = strtolower( sanitize_title( str_replace( 'pa_', '', $domain ) ) );
			$exists_in_db = $wpdb->get_var(
				$wpdb->prepare(
					"SELECT attribute_id FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_name = %s;",
					$nicename
				)
			);

			// Create the taxonomy
			if ( ! $exists_in_db ) {
				$wpdb->insert(
					"{$wpdb->prefix}woocommerce_attribute_taxonomies",
					array(
						'attribute_name'    => $nicename,
						'attribute_type'    => 'select',
						'attribute_orderby' => 'menu_order',
					),
					array( '%s', '%s', '%s' )
				);
			}

			// Register the taxonomy now so that the import works!
			$tax_args = array(
				'hierarchical' => true,
				'show_ui'      => false,
				'query_var'    => true,
				'rewrite'      => false,
			);
			// Important! Allows us to pass Envato Theme Checker tests.
			$func = 'register_taxonomy';
			$func(
				$domain,
				apply_filters( 'woocommerce_taxonomy_objects_' . $domain, array( 'product' ) ),
				apply_filters( 'woocommerce_taxonomy_args_' . $domain, $tax_args )
			);
		}

		return $raw_post;
	}

	/**
	 * If fetching attachments is enabled then attempt to create a new attachment
	 *
	 * @param array  $post Attachment post details from WXR.
	 * @param string $url URL to fetch attachment from.
	 *
	 * @return int|WP_Error Post ID on success, WP_Error otherwise.
	 */
	public function process_attachment( $post, $url ) {

		// Save elementor stuff in the separate folder.
		if ( strpos( $url, '/elementor/' ) !== false ) {
			$path_bits                 = explode( '/elementor/', wp_parse_url( $url, PHP_URL_PATH ) );
			$this->elementor_file_path = dirname( '/elementor/' . $path_bits[1] );

			add_filter( 'upload_dir', [ $this, 'uploads_filter' ] );
		}

		$this->log_add( 'Importing [attachment] ' . $url );
		$start = microtime( true );

		$result = parent::process_attachment( $post, $url );

		$this->log_add( 'Loaded in ' . ( microtime( true ) - $start ) . "\n" );

		remove_filter( 'upload_dir', [ $this, 'uploads_filter' ] );

		return $result;
	}

	public function uploads_filter( $uploads ) {
		if ( isset( $this->elementor_file_path ) ) {
			$uploads['path'] = $uploads['basedir'] . $this->elementor_file_path;
		}

		return $uploads;
	}

	/**
	 * Cache processed data to be used in future imports.
	 */
	public function cache_processed_data() {
		self::clear_session();

		$data = [
			'processed_authors' => $this->processed_authors,
			'processed_terms'   => $this->processed_terms,
			'processed_posts'   => $this->processed_posts,
			'featured_images'   => $this->featured_images,
			'url_remap'         => $this->url_remap,
			'base_url'			=> $this->base_url,
		];
		set_transient( self::THE7_PROCESSED_DATA_KEY, $data, 1200 );
	}

	/**
	 * Read populate internal variables with processed data from cache.
	 */
	public function read_processed_data_from_cache() {
		$this->log_add( "\n" . str_repeat( '-', 30 ) );

		$processed_data = get_transient( self::THE7_PROCESSED_DATA_KEY );
		if ( ! is_array( $processed_data ) || empty( $processed_data ) ) {
			$this->log_add( 'There is no session stored.' );
			return;
		}

		$this->log_add( 'Restore session:' );

		foreach ( $processed_data as $group => $data ) {
			if ( property_exists( $this, $group ) && $data ) {
				$this->$group = $data;
				$this->log_add(
					sprintf(
						'[%1$s] with %2$s items.',
						$group,
						count( (array) $data )
					)
				);
			}
		}

		$this->log_add( str_repeat( '-', 30 ) . "\n" );
	}

	public static function clear_session() {
		delete_transient( self::THE7_PROCESSED_DATA_KEY );
	}

	/**
	 * Decide whether or not the importer is allowed to create users.
	 * Default is false (we don't want to scare users), can be filtered via import_allow_create_users.
	 *
	 * @return bool True if creating users is allowed
	 */
	public function allow_create_users() {
		return apply_filters( 'import_allow_create_users', false );
	}

	public function get_processed_post( $post_id ) {
		if ( isset( $this->processed_posts[ $post_id ] ) ) {
			return $this->processed_posts[ $post_id ];
		}

		return 0;
	}

	public function get_processed_filtered_post() {
		return $this->get_processed_post( $this->filtered_post_id );
	}

	/**
	 * Returns the post ID, found in demo content data, after applying a filter (by url or by id).
	 *
	 * @return null|int
	 */
	public function get_filtered_post_id() {
		return $this->filtered_post_id;
	}

	public function get_processed_term( $term_id ) {
		if ( isset( $this->processed_terms[ $term_id ] ) ) {
			return $this->processed_terms[ $term_id ];
		}

		return 0;
	}

	public function get_processed_taxonomy_id( $tax_id ) {
		if ( isset( $this->processed_taxonomies[ $tax_id ] ) ) {
			return $this->processed_taxonomies[ $tax_id ];
		}

		return 0;
	}

	/**
	 * @since 10.4.3
	 *
	 * @param int $id Author id.
	 *
	 * @return int
	 */
	public function get_processed_author_id( $id ) {
		if ( isset( $this->processed_authors[ $id ] ) ) {
			return $this->processed_authors[ $id ];
		}

		return 0;
	}

	public function add_filter_by_url( $post_url ) {
		$this->filter_by_url = $post_url;
		$this->add_import_posts_filter();
	}

	public function add_filter_by_id( $post_id ) {
		$this->filter_by_id = $post_id;
		$this->add_import_posts_filter();
	}

	/**
	 * Reset the log.
	 */
	public function log_reset() {
		delete_transient( 'the7_import_log' );
	}

	/**
	 * Add a message to the log.
	 *
	 * @param string $message Message.
	 */
	public function log_add( $message ) {
		if ( $message ) {
			set_transient( 'the7_import_log', $this->log_get() . "\n" . $message, WEEK_IN_SECONDS );
		}
	}

	/**
	 * @return string
	 */
	public function log_get() {
		return (string) get_transient( 'the7_import_log' );
	}

	/**
	 * @return void
	 */
	public function remap_post_ids_in_blocks() {
		global $wpdb;

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

		$posts_ids = implode( ',', array_map( 'intval', $this->processed_posts ) );
		$posts     = $wpdb->get_results( "SELECT * FROM $wpdb->posts WHERE post_content != '' AND post_status = 'publish' AND ID IN ({$posts_ids})" );

		$home_url = home_url();

		foreach ( $posts as $post ) {
			$updated      = [];
			$post_content = $post->post_content;

			$urls = $this->get_non_attachment_urls( $post_content );

			foreach ( $urls as $url ) {
				$new_url      = str_replace( $this->base_url, $home_url, $url );
				$post_content = str_replace( $url, $new_url, $post_content );
			}

			$blocks        = parse_blocks( $post_content );
			$edited_blocks = $this->iterate_blocks(
				$blocks,
				function ( $block ) use ( &$updated ) {
					if ( empty( $block['attrs'] ) ) {
						return $block;
					}

					$attrs = $block['attrs'];

					if ( isset( $attrs['id'] ) ) {
						$id = $attrs['id'];
						if ( ! empty( $this->processed_posts[ $id ] ) ) {
							$attrs['id'] = $this->processed_posts[ $id ];
							$updated[]   = [
								'block'  => $block,
								'old_id' => $id,
								'new_id' => $attrs['id'],
							];
						}
					}

					if ( isset( $attrs['ref'] ) ) {
						$ref = $attrs['ref'];
						if ( ! empty( $this->processed_posts[ $ref ] ) ) {
							$attrs['ref'] = $this->processed_posts[ $ref ];
							$updated[]    = [
								'block'   => $block,
								'old_ref' => $ref,
								'new_ref' => $attrs['ref'],
							];
						}
					}

					$block['attrs'] = $attrs;

					return $block;
				}
			);

			wp_update_post(
				[
					'ID'           => $post->ID,
					'post_content' => wp_slash( serialize_blocks( $edited_blocks ) ),
				]
			);
		}
	}

	/**
	 * @param array    $blocks
	 * @param callable $callback
	 *
	 * @return array
	 */
	protected function iterate_blocks( $blocks, $callback ) {
		foreach ( $blocks as $i => $block ) {
			if ( ! empty( $block['innerBlocks'] ) ) {
				$block['innerBlocks'] = $this->iterate_blocks( $block['innerBlocks'], $callback );
			}

			$blocks[ $i ] = $callback( $block );
		}

		return $blocks;
	}

	/**
	 * @param string $content
	 *
	 * @return array
	 */
	protected function get_non_attachment_urls( $content ) {
		$regex = '~
    \bhttps?://                     # Start with http:// or https://
    [^\s"\'<>]+                     # Match all non-space, non-quote, non-bracket characters
    (?=["\'\s<>]|$)                 # Stop at quote, space, bracket, or end of string
    ~xi';

		preg_match_all( $regex, $content, $matches );

		$urls = $matches[0];

		// Filter out attachment urls.
		$urls = array_filter(
			$urls,
			function ( $url ) {
				return strpos( $url, 'wp-content/uploads' ) === false;
			}
		);

		// Remove any potential trailing commas or quotes.
		$clean_urls = array_map(
			function ( $url ) {
				return rtrim( $url, '",' );
			},
			$urls
		);

		return array_unique( $clean_urls );
	}

	protected function find_post_by_url( $posts ) {
		foreach ( $posts as $post ) {
			if ( (string) $post['link'] === (string) $this->filter_by_url ) {
				return $post;
			}
		}

		return [];
	}

	protected function find_post_by_id( $posts ) {
		foreach ( $posts as $post ) {
			if ( (int) $post['post_id'] === (int) $this->filter_by_id ) {
				return $post;
			}
		}

		return [];
	}

	protected function add_import_posts_filter() {
		add_filter(
			'wp_import_parse',
			function( $parsed_file ) {
				$posts = $parsed_file['posts'];

				$this->log_add( 'Filtering ' . count( $posts ) . ' posts by [' . ( $this->filter_by_url ?: $this->filter_by_id ) . ']' );

				if ( $this->filter_by_url ) {
					$found_post = $this->find_post_by_url( $posts );
				} elseif ( $this->filter_by_id ) {
					$found_post = $this->find_post_by_id( $posts );
				}

				if ( empty( $found_post ) ) {
					$this->log_add( 'Requested post was not found' );

					return [];
				}

				$this->log_add( "Post with ID [{$found_post['post_id']}] was found" );

				$this->filtered_post_id = $found_post['post_id'];
				$filtered_posts         = [ $found_post ];

				$resources = array_reduce(
					(array) $found_post['postmeta'],
					function ( $acc, $item ) {
						return $item['key'] === static::THE7_POST_RESOURCES_META_KEY ? maybe_unserialize( $item['value'] ) : $acc;
					},
					[]
				);

				$this->log_add( 'Found ' . count( $resources ) . ' resources' );

				$posts_terms = [];
				foreach ( $posts as $post ) {
					if ( in_array( $post['post_id'], $resources, false ) ) {
						if ( $post['post_type'] === 'elementor_library' && ! empty( $post['postmeta'] ) ) {
							foreach ( $post['postmeta'] as &$post_meta ) {
								if ( $post_meta['key'] === '_elementor_conditions' ) {
									if ( $found_post['post_type'] === 'product' ) {
										$value = [
											'include/woocommerce/product/' . $this->filtered_post_id,
										];
									} else {
										$value = [
											'include/singular/' . $found_post['post_type'] . '/' . $this->filtered_post_id,
										];
									}
									$post_meta['value'] = maybe_serialize( $value );
									break;
								}
							}
							unset( $post_meta );
						}

						if ( ! empty( $post['terms'] ) ) {
							$posts_terms[] = $post['terms'];
						}

						$filtered_posts[] = $post;
					}
				}

				$posts_terms = array_merge( [], ...$posts_terms );
				$posts_terms = array_unique( wp_list_pluck( $posts_terms, 'slug' ) );

				$parsed_file['posts'] = $filtered_posts;

				$parsed_file['categories'] = array_filter(
					$parsed_file['categories'],
					function( $term ) use ( $posts_terms ) {
						return in_array( $term['category_nicename'], $posts_terms, true );
					}
				);

				$parsed_file['tags'] = array_filter(
					$parsed_file['tags'],
					function( $term ) use ( $posts_terms ) {
						return in_array( $term['tag_slug'], $posts_terms, true );
					}
				);

				$parsed_file['terms'] = array_filter(
					$parsed_file['terms'],
					function( $term ) use ( $posts_terms ) {
						return in_array( $term['slug'], $posts_terms, true );
					}
				);

				return $parsed_file;
			}
		);
	}

	/**
	 * @param string $wp_attached_file
	 *
	 * @return int
	 */
	protected function attachment_exists( $wp_attached_file ) {
		global $wpdb;

		return (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT post_id FROM {$wpdb->postmeta} WHERE 1=1 AND meta_key = '_wp_attached_file' AND meta_value = %s",
				$wp_attached_file
			)
		);
	}
}
