<?php
/**
 * Native page integration.
 *
 * Registers builder meta on WordPress pages and keeps generated
 * AM Builder assets in sync when those pages are saved.
 *
 * @package Antimanual_Builder
 * @since   1.0.0
 */

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

/**
 * Class AMBLD_Page_Integration
 *
 * Hooks AM Builder into the core `page` post type.
 *
 * @since 1.0.0
 */
class AMBLD_Page_Integration {

	/**
	 * Supported post type slug.
	 *
	 * @since 1.0.0
	 * @var   string
	 */
	const POST_TYPE = 'page';

	/**
	 * True while a builder REST save is in progress. The REST controllers
	 * fire `ambld_page_saved` themselves AFTER the builder meta is written;
	 * the `save_post_page`-driven firing below would run first with STALE
	 * meta and generate every asset twice per save.
	 *
	 * @since 0.4.0
	 * @var   bool
	 */
	public static $suppress_save_hook = false;

	/**
	 * Constructor. Registers hooks for page integration.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		add_action( 'init', array( $this, 'register_meta' ) );
		add_action( 'save_post_' . self::POST_TYPE, array( $this, 'save_meta' ), 10, 2 );
		add_filter( 'wp_kses_allowed_html', array( $this, 'expand_kses_for_shortcode_blocks' ), 10, 2 );
		// Priority 12: after wpautop (10) and do_shortcode (11). We replace
		// the content entirely, so wpautop's mangling of the synced copy is
		// discarded rather than fought.
		add_filter( 'the_content', array( $this, 'render_builder_content' ), 12 );

		// Native admin actions (quick-edit, trash/restore/delete from the
		// Pages screen) must invalidate the builder's pages-list cache too —
		// only REST saves bumped it, leaving the editor's page list stale
		// for up to a minute after admin-side changes.
		add_action( 'save_post_' . self::POST_TYPE, array( $this, 'bump_pages_list_cache' ) );
		add_action( 'trashed_post', array( $this, 'bump_pages_list_cache_for_page' ) );
		add_action( 'untrashed_post', array( $this, 'bump_pages_list_cache_for_page' ) );
		add_action( 'deleted_post', array( $this, 'bump_pages_list_cache_for_page' ) );
	}

	/**
	 * Bump the pages-list cache version (used as a cache-buster suffix).
	 *
	 * @since 0.4.0
	 * @return void
	 */
	public function bump_pages_list_cache() {
		update_option( 'ambld_pages_list_cache_version', time(), false );
	}

	/**
	 * Bump the list cache when the affected post is a page.
	 *
	 * @since 0.4.0
	 * @param int $post_id Post ID.
	 * @return void
	 */
	public function bump_pages_list_cache_for_page( $post_id ) {
		if ( self::POST_TYPE === get_post_type( $post_id ) ) {
			$this->bump_pages_list_cache();
		}
	}

	/**
	 * Render builder content when a THEME template displays a builder page.
	 *
	 * The builder's own templates (canvas, full-width, standalone) never
	 * call the_content — they run the render pipeline themselves. But when
	 * the user assigns a theme `.php` template (or "Default"), the theme
	 * renders `post_content`, which is only a plain synced copy of the
	 * builder HTML: wpautop injected stray <p>/<br> tags, shortcode markers
	 * showed their sample content, and dynamic {{tokens}} leaked verbatim.
	 * This filter swaps in the real pipeline output.
	 *
	 * @since  0.4.0
	 * @param  string $content Theme-filtered post content.
	 * @return string
	 */
	public function render_builder_content( $content ) {
		if ( ! in_the_loop() || ! is_main_query() || ! is_singular( self::POST_TYPE ) ) {
			return $content;
		}

		$post_id = get_the_ID();
		if ( ! $post_id || ! self::is_builder_page( $post_id ) ) {
			return $content;
		}

		$rendered = (string) get_post_meta( $post_id, '_ambld_rendered_html', true );
		if ( '' === trim( $rendered ) ) {
			return $content;
		}

		return AMBLD_Render_Pipeline::prepare( $rendered, $post_id, 'canvas' );
	}

	/**
	 * Expand kses allowed HTML to permit shortcode block data attributes.
	 *
	 * This allows editor-only users (without unfiltered_html capability)
	 * to save pages containing data-shortcode markers without the markers
	 * being stripped by wp_kses_post sanitization.
	 *
	 * @since  1.0.0
	 * @param  array  $allowed Allowed HTML tags and attributes.
	 * @param  string $context Context for filtering (e.g. 'post', 'strip').
	 * @return array
	 */
	public function expand_kses_for_shortcode_blocks( $allowed, $context ) {
		if ( 'post' !== $context ) {
			return $allowed;
		}

		if ( ! isset( $allowed['div'] ) ) {
			$allowed['div'] = array();
		}

		$allowed['div']['data-shortcode'] = true;
		$allowed['div']['data-amb-shortcode-id'] = true;

		$global_attrs = array(
			'class'            => true,
			'id'               => true,
			'style'            => true,
			'aria-label'       => true,
			'aria-describedby' => true,
			'aria-hidden'      => true,
			'role'             => true,
			'tabindex'         => true,
		);

		$form_tags = array(
			'form'     => array_merge( $global_attrs, array(
				'action'         => true,
				'method'         => true,
				'enctype'        => true,
				'accept-charset' => true,
				'autocomplete'   => true,
				'name'           => true,
				'novalidate'     => true,
				'target'         => true,
			) ),
			'input'    => array_merge( $global_attrs, array(
				'type'         => true,
				'name'         => true,
				'value'        => true,
				'placeholder'  => true,
				'required'     => true,
				'readonly'     => true,
				'disabled'     => true,
				'checked'      => true,
				'min'          => true,
				'max'          => true,
				'step'         => true,
				'minlength'    => true,
				'maxlength'    => true,
				'pattern'      => true,
				'autocomplete' => true,
				'autofocus'    => true,
				'multiple'     => true,
				'accept'       => true,
				'list'         => true,
				'size'         => true,
			) ),
			'textarea' => array_merge( $global_attrs, array(
				'name'         => true,
				'rows'         => true,
				'cols'         => true,
				'placeholder'  => true,
				'required'     => true,
				'readonly'     => true,
				'disabled'     => true,
				'minlength'    => true,
				'maxlength'    => true,
				'wrap'         => true,
				'autocomplete' => true,
			) ),
			'select'   => array_merge( $global_attrs, array(
				'name'         => true,
				'multiple'     => true,
				'size'         => true,
				'required'     => true,
				'disabled'     => true,
				'autocomplete' => true,
			) ),
			'option'   => array_merge( $global_attrs, array(
				'value'    => true,
				'selected' => true,
				'disabled' => true,
				'label'    => true,
			) ),
			'optgroup' => array_merge( $global_attrs, array(
				'label'    => true,
				'disabled' => true,
			) ),
			'label'    => array_merge( $global_attrs, array(
				'for' => true,
			) ),
			'fieldset' => array_merge( $global_attrs, array(
				'name'     => true,
				'disabled' => true,
				'form'     => true,
			) ),
			'legend'   => $global_attrs,
			'datalist' => $global_attrs,
			'output'   => array_merge( $global_attrs, array(
				'for'  => true,
				'name' => true,
				'form' => true,
			) ),
			'progress' => array_merge( $global_attrs, array(
				'value' => true,
				'max'   => true,
			) ),
			'meter'    => array_merge( $global_attrs, array(
				'value'   => true,
				'min'     => true,
				'max'     => true,
				'low'     => true,
				'high'    => true,
				'optimum' => true,
			) ),
		);

		$html5_tags = array(
			'dialog'   => array_merge( $global_attrs, array(
				'open' => true,
			) ),
			'picture'  => $global_attrs,
			'canvas'   => array_merge( $global_attrs, array(
				'width'  => true,
				'height' => true,
			) ),
			'template' => $global_attrs,
			'slot'     => array_merge( $global_attrs, array(
				'name' => true,
			) ),
			'data'     => array_merge( $global_attrs, array(
				'value' => true,
			) ),
			'bdi'      => $global_attrs,
			'wbr'      => $global_attrs,
			'ruby'     => $global_attrs,
			'rt'       => $global_attrs,
			'rp'       => $global_attrs,
			'menu'     => array_merge( $global_attrs, array(
				'type' => true,
			) ),
			'noscript' => $global_attrs,
		);

		foreach ( $form_tags as $tag => $attrs ) {
			$allowed[ $tag ] = isset( $allowed[ $tag ] ) && is_array( $allowed[ $tag ] )
				? array_merge( $allowed[ $tag ], $attrs )
				: $attrs;
		}

		foreach ( $html5_tags as $tag => $attrs ) {
			$allowed[ $tag ] = isset( $allowed[ $tag ] ) && is_array( $allowed[ $tag ] )
				? array_merge( $allowed[ $tag ], $attrs )
				: $attrs;
		}

		return $allowed;
	}

	/**
	 * Register page meta fields for REST API access.
	 *
	 * @since  1.0.0
	 * @return void
	 */
	public function register_meta() {
		register_post_meta(
			self::POST_TYPE,
			'_ambld_blocks',
			array(
				'type'              => 'string',
				'single'            => true,
				'show_in_rest'      => true,
				'sanitize_callback' => 'sanitize_text_field',
				'auth_callback'     => function () {
					return current_user_can( 'edit_pages' );
				},
			)
		);

		register_post_meta(
			self::POST_TYPE,
			'_ambld_rendered_html',
			array(
				'type'              => 'string',
				'single'            => true,
				'show_in_rest'      => true,
				'sanitize_callback' => function ( $html ) {
					return current_user_can( 'unfiltered_html' ) ? $html : wp_kses_post( $html );
				},
				'auth_callback'     => function () {
					return current_user_can( 'edit_pages' );
				},
			)
		);

		register_post_meta(
			self::POST_TYPE,
			'_ambld_page_settings',
			array(
				'type'              => 'string',
				'single'            => true,
				'show_in_rest'      => true,
				'sanitize_callback' => function ( $value ) {
					// Page settings are JSON-encoded objects that may contain trusted
					// import markup (e.g. <script> tags). Sanitization is handled by
					// sanitize_builder_page_settings_meta_value() in the REST API layer,
					// so we only strip slashes here rather than using sanitize_text_field
					// which would strip HTML tags and break the stored JSON.
					return is_string( $value ) ? $value : '';
				},
				'auth_callback'     => function () {
					return current_user_can( 'edit_pages' );
				},
			)
		);

		register_post_meta(
			self::POST_TYPE,
			'_ambld_style_map',
			array(
				'type'              => 'string',
				'single'            => true,
				'show_in_rest'      => true,
				'sanitize_callback' => function ( $value ) {
					return is_string( $value ) ? $value : '';
				},
				'auth_callback'     => function () {
					return current_user_can( 'edit_pages' );
				},
			)
		);

		register_post_meta(
			self::POST_TYPE,
			'_ambld_builder_enabled',
			array(
				'type'              => 'boolean',
				'single'            => true,
				'show_in_rest'      => true,
				'sanitize_callback' => 'rest_sanitize_boolean',
				'auth_callback'     => function () {
					return current_user_can( 'edit_pages' );
				},
			)
		);
	}

	/**
	 * Handle page saves.
	 *
	 * @since  1.0.0
	 * @param  int     $post_id Post ID.
	 * @param  WP_Post $post    Post object.
	 * @return void
	 */
	public function save_meta( $post_id, $post ) {
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}

		if ( wp_is_post_revision( $post_id ) ) {
			return;
		}

		// Builder REST saves fire ambld_page_saved themselves with fresh meta.
		if ( self::$suppress_save_hook ) {
			return;
		}

		// Asset generation only applies to builder-managed pages; running it
		// for every native page save sitewide is wasted file/DB churn.
		if ( ! self::is_builder_page( $post ) ) {
			return;
		}

		do_action( 'ambld_page_saved', $post_id, $post );
	}

	/**
	 * Determine whether a page is already managed by AM Builder.
	 *
	 * @since  1.0.0
	 * @param  int|WP_Post|null $post Post object or ID.
	 * @return bool
	 */
	public static function is_builder_page( $post ) {
		$post = self::resolve_post( $post );

		if ( ! $post ) {
			return false;
		}

		$enabled = get_post_meta( $post->ID, '_ambld_builder_enabled', true );
		if ( ! empty( $enabled ) ) {
			return true;
		}

		$meta_keys = array(
			'_ambld_blocks',
			'_ambld_rendered_html',
		);

		foreach ( $meta_keys as $meta_key ) {
			$value = get_post_meta( $post->ID, $meta_key, true );

			if ( is_string( $value ) && '' !== trim( $value ) ) {
				return true;
			}
		}

		// Check for CSS source file in uploads.
		if ( file_exists( AMBLD_Render_Asset_Generator::get_source_file_path( $post->ID, 'css' ) ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Determine whether a page can be migrated into AM Builder.
	 *
	 * @since  1.0.0
	 * @param  int|WP_Post|null $post Post object or ID.
	 * @return bool
	 */
	public static function has_migratable_content( $post ) {
		$post = self::resolve_post( $post );

		if ( ! $post || self::is_builder_page( $post ) ) {
			return false;
		}

		if ( in_array( $post->post_status, array( 'auto-draft', 'trash' ), true ) ) {
			return false;
		}

		$content = preg_replace( '/<!--[\s\S]*?-->/', '', (string) $post->post_content );
		$content = trim( wp_strip_all_tags( $content ) );

		return '' !== $content;
	}

	/**
	 * Resolve and validate a native page post.
	 *
	 * @since  1.0.0
	 * @param  int|WP_Post|null $post Post object or ID.
	 * @return WP_Post|null
	 */
	private static function resolve_post( $post ) {
		if ( is_numeric( $post ) ) {
			$post = get_post( (int) $post );
		}

		if ( ! ( $post instanceof \WP_Post ) || self::POST_TYPE !== $post->post_type ) {
			return null;
		}

		return $post;
	}
}
