<?php
/**
 * Mega panel post type.
 *
 * @package MenuVerse
 */

namespace WPXero\MenuVerse\Module\Mega_Menu;

use WPXero\MenuVerse\Capabilities;
use WPXero\MenuVerse\Settings\Schema;
use WPXero\MenuVerse\Settings\Schema_Sanitizer;

defined( 'ABSPATH' ) || exit;

/**
 * Stores mega panel content as blocks.
 *
 * Panels are a post type rather than menu item meta because their bodies are
 * blocks: only a post type gets native block editing, revisions and the REST
 * entity the dashboard editor loads.
 */
final class Panel_Post_Type {

	/**
	 * Post type name.
	 *
	 * @var string
	 */
	const POST_TYPE = 'menuverse_panel';

	/**
	 * Meta key holding panel layout settings.
	 *
	 * @var string
	 */
	const META_KEY = '_menuverse_panel';

	/**
	 * Cron hook that removes panels no menu item points at.
	 *
	 * @var string
	 */
	const CLEANUP_HOOK = 'menuverse_cleanup_panels';

	/**
	 * Registers hooks.
	 *
	 * @return void
	 */
	public function boot() {
		add_action( 'init', array( $this, 'register' ), 4 );
		add_action( self::CLEANUP_HOOK, array( $this, 'cleanup' ) );
	}

	/**
	 * Returns the schema for a panel's layout settings.
	 *
	 * @return array
	 */
	public static function meta_schema() {
		$schema = array(
			'type'       => 'object',
			'properties' => array(
				'width'      => array(
					'type'    => 'string',
					'default' => 'inherit',
					'enum'    => array( 'inherit', 'contained', 'full' ),
				),
				'columns'    => array(
					'type'    => 'integer',
					'default' => 3,
					'minimum' => 1,
					'maximum' => 6,
				),
				'background' => array(
					'type'    => 'string',
					'format'  => 'color',
					'default' => '',
				),
				'padding'    => array(
					'type'    => 'string',
					'format'  => 'css-size',
					'default' => '24px',
				),
			),
		);

		/**
		 * Filters the mega panel layout schema.
		 *
		 * @since 1.0.0
		 *
		 * @param array $schema Schema definition.
		 */
		return apply_filters( 'menuverse_panel_meta_schema', $schema );
	}

	/**
	 * Registers the post type, its meta and the cleanup event.
	 *
	 * @return void
	 */
	public function register() {
		$capability = Capabilities::manage();

		register_post_type(
			self::POST_TYPE,
			array(
				'labels'                => array(
					'name'          => __( 'Mega panels', 'menuverse' ),
					'singular_name' => __( 'Mega panel', 'menuverse' ),
					'add_new_item'  => __( 'Add mega panel', 'menuverse' ),
					'edit_item'     => __( 'Edit mega panel', 'menuverse' ),
				),
				'public'                => false,
				'show_ui'               => false,
				'show_in_menu'          => false,
				'show_in_rest'          => true,
				'rest_base'             => 'menuverse-panels',
				'rest_controller_class' => Panel_Rest_Controller::class,
				'rewrite'               => false,
				'query_var'             => false,
				'hierarchical'          => false,
				'supports'              => array( 'title', 'editor', 'revisions', 'custom-fields' ),
				'delete_with_user'      => false,
				'can_export'            => true,
				'capability_type'       => array( 'menuverse_panel', 'menuverse_panels' ),
				'map_meta_cap'          => true,
				// Only the primitive capabilities are remapped. Pointing the
				// meta capabilities (edit_post, read_post, delete_post) at the
				// same name would make WordPress treat menuverse_manage itself
				// as a meta capability, and every current_user_can() check for
				// it would then fail for want of a post ID.
				'capabilities'          => array(
					'edit_posts'             => $capability,
					'edit_others_posts'      => $capability,
					'edit_published_posts'   => $capability,
					'edit_private_posts'     => $capability,
					'publish_posts'          => $capability,
					'read_private_posts'     => $capability,
					'delete_posts'           => $capability,
					'delete_others_posts'    => $capability,
					'delete_published_posts' => $capability,
					'delete_private_posts'   => $capability,
					'create_posts'           => $capability,
				),
				'template'              => array(
					array(
						'core/columns',
						array(),
						array(
							array(
								'core/column',
								array(),
								array(
									array(
										'core/heading',
										array(
											'level'   => 3,
											'content' => '',
										),
									),
									array( 'core/list' ),
								),
							),
							array(
								'core/column',
								array(),
								array(
									array(
										'core/heading',
										array(
											'level'   => 3,
											'content' => '',
										),
									),
									array( 'core/list' ),
								),
							),
							array( 'core/column', array(), array( array( 'core/image' ) ) ),
						),
					),
				),
			)
		);

		register_post_meta(
			self::POST_TYPE,
			self::META_KEY,
			array(
				'single'            => true,
				'type'              => 'object',
				'default'           => Schema::defaults( self::meta_schema() ),
				'show_in_rest'      => array(
					'schema' => Schema::strip_ui( self::meta_schema() ),
				),
				'sanitize_callback' => array( __CLASS__, 'sanitize_meta' ),
				'auth_callback'     => array( Capabilities::class, 'current_user_can_manage' ),
			)
		);

		if ( ! wp_next_scheduled( self::CLEANUP_HOOK ) ) {
			wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', self::CLEANUP_HOOK );
		}
	}

	/**
	 * Sanitizes panel layout settings against the schema.
	 *
	 * @param mixed $value Raw meta value.
	 * @return array
	 */
	public static function sanitize_meta( $value ) {
		return Schema_Sanitizer::sanitize( is_array( $value ) ? $value : array(), self::meta_schema() );
	}

	/**
	 * Returns the layout settings for a panel.
	 *
	 * @param int $panel_id Panel post ID.
	 * @return array
	 */
	public static function settings( $panel_id ) {
		$stored = get_post_meta( (int) $panel_id, self::META_KEY, true );

		return Schema_Sanitizer::sanitize( is_array( $stored ) ? $stored : array(), self::meta_schema() );
	}

	/**
	 * Returns every panel, newest first.
	 *
	 * @return \WP_Post[]
	 */
	public static function all() {
		return get_posts(
			array(
				'post_type'              => self::POST_TYPE,
				'post_status'            => array( 'publish', 'draft' ),
				'posts_per_page'         => 100,
				'orderby'                => 'title',
				'order'                  => 'ASC',
				'no_found_rows'          => true,
				'update_post_term_cache' => false,
			)
		);
	}

	/**
	 * Returns the panel IDs referenced by any menu item.
	 *
	 * @return int[]
	 */
	public static function attached_ids() {
		global $wpdb;

		$rows = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- runs from a daily cron event only.
			$wpdb->prepare(
				"SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s",
				\WPXero\MenuVerse\Menus\Item_Extras::META_KEY
			)
		);

		$ids = array();

		foreach ( $rows as $row ) {
			$extras = maybe_unserialize( $row );

			if ( is_array( $extras ) && ! empty( $extras['panel_id'] ) ) {
				$ids[] = (int) $extras['panel_id'];
			}
		}

		/**
		 * Filters the panel IDs considered to be in use.
		 *
		 * Extensions that attach panels somewhere else must add their IDs here
		 * or the cleanup event will treat those panels as orphans.
		 *
		 * @since 1.0.0
		 *
		 * @param int[] $ids Panel IDs in use.
		 */
		return array_unique( (array) apply_filters( 'menuverse_attached_panel_ids', $ids ) );
	}

	/**
	 * Moves panels that nothing points at into the trash.
	 *
	 * Trash rather than delete: an accidental menu edit should not destroy a
	 * panel somebody spent an afternoon building.
	 *
	 * @return void
	 */
	public function cleanup() {
		$attached = self::attached_ids();

		foreach ( self::all() as $panel ) {
			if ( in_array( (int) $panel->ID, $attached, true ) ) {
				continue;
			}

			$age = time() - (int) get_post_time( 'U', true, $panel );

			if ( $age > 7 * DAY_IN_SECONDS ) {
				wp_trash_post( $panel->ID );
			}
		}
	}
}
