<?php
/**
 * Engine: content discovery. Enumerates auditable content via WP_Query by post
 * ID - the engine NEVER crawls, spiders, or follows links. The set of post
 * types and statuses comes from boot config, with filter seams so add-on files
 * can extend them (e.g. premium CPT / draft scope).
 *
 * Generalized from SSAA's SSAA_Content_Discovery: the post/page half returns
 * descriptors (not bare IDs) so it composes with the consumer's synthesized
 * non-post descriptors, and the excluded-refs store lives in
 * SSAA_Engine_Exclusions (descriptor-aware) rather than here.
 *
 * Password-protected content is never returned (has_password => false): a
 * crawler/scraper only ever sees the password form, so auditing it is
 * meaningless. Private/draft statuses are simply not in the default status
 * list; a consumer's premium scope can add them via the status filter.
 *
 * CANONICAL ENGINE SOURCE - edit here in _shared/shared-render-engine/src/, re-run sync.sh.
 *
 * @package plugupp-render-engine
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class SSAA_Engine_Discovery {

	/**
	 * Post types eligible for scanning. Config 'post_types' (default
	 * post + page) through the '{prefix}_discovery_post_types' filter.
	 *
	 * @return string[]
	 */
	public static function post_types() {
		$enabled = (array) SSAA_Engine::config( 'post_types', array( 'post', 'page' ) );
		$types   = (array) apply_filters( SSAA_Engine::key( 'discovery_post_types' ), $enabled );
		return array_values( array_unique( array_filter( array_map( 'sanitize_key', $types ) ) ) );
	}

	/**
	 * Post statuses eligible for scanning. Config 'post_statuses' (default
	 * publish) through the '{prefix}_discovery_post_statuses' filter.
	 *
	 * @return string[]
	 */
	public static function post_statuses() {
		$enabled  = (array) SSAA_Engine::config( 'post_statuses', array( 'publish' ) );
		$statuses = (array) apply_filters( SSAA_Engine::key( 'discovery_post_statuses' ), $enabled );
		return array_values( array_filter( array_map( 'sanitize_key', $statuses ) ) );
	}

	/**
	 * IDs of the post-type content items a scan should cover, most recent
	 * first, with excluded posts removed.
	 *
	 * @return int[]
	 */
	public static function get_post_ids() {
		$query = new WP_Query( array(
			'post_type'              => self::post_types(),
			'post_status'            => self::post_statuses(),
			'has_password'           => false, // Skip password-protected posts: crawlers/scrapers only ever see the password form.
			// phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- the excluded set is a small, admin-curated list; this is an admin-only audit query (ids only), not a front-end query.
			'post__not_in'           => SSAA_Engine_Exclusions::excluded_post_ids(),
			'posts_per_page'         => -1, // No item limit - the free plugin scans all published pages and posts.
			'orderby'                => 'date',
			'order'                  => 'DESC',
			'fields'                 => 'ids',
			'no_found_rows'          => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
			'ignore_sticky_posts'    => true,
		) );

		return array_map( 'intval', $query->posts );
	}

	/**
	 * Page descriptors for all in-scope post-type content, most recent first.
	 * This is the post/page half of discovery; a consumer's handler typically
	 * appends synthesized non-post (archive) descriptors to this list.
	 *
	 * @return array[] List of 'post'-kind descriptors.
	 */
	public static function post_descriptors() {
		$descriptors = array();
		foreach ( self::get_post_ids() as $post_id ) {
			$descriptor = self::describe_post( $post_id );
			if ( null !== $descriptor ) {
				$descriptors[] = $descriptor;
			}
		}
		return $descriptors;
	}

	/**
	 * Descriptor for one post (queue payload, list rows). Null when the post no
	 * longer exists.
	 *
	 * The shape is the engine descriptor envelope ({kind, ref, type, label})
	 * plus the post fields a consumer commonly persists.
	 *
	 * @param int $post_id Post ID.
	 * @return array|null
	 */
	public static function describe_post( $post_id ) {
		$post = get_post( $post_id );
		if ( ! $post ) {
			return null;
		}

		$author = '';
		if ( post_type_supports( $post->post_type, 'author' ) && $post->post_author ) {
			$author = (string) get_the_author_meta( 'display_name', (int) $post->post_author );
		}

		return array(
			'kind'        => 'post',
			'ref'         => SSAA_Engine::post_ref( (int) $post->ID ),
			'type'        => $post->post_type,
			'label'       => get_the_title( $post ),
			'permalink'   => get_permalink( $post ),
			// Common consumer payload (mirrors SSAA's describe()).
			'post_id'     => (int) $post->ID,
			'post_status' => $post->post_status,
			'author'      => $author,
			'modified'    => $post->post_modified_gmt,
			'edit_link'   => get_edit_post_link( $post->ID, 'raw' ),
		);
	}
}
