<?php
/**
 * TitanCart Pages Module
 *
 * The thin WordPress wiring layer for the Page Manager (roadmap #2). The
 * canonical registry itself — PageRegistry (S343) — is a pure static utility
 * with zero always-on code path; this module simply exposes it to the admin
 * SPA over REST.
 *
 * Session 2 (S344): registers PagesController on rest_api_init.
 *
 * Session 3 (S345): adds the runtime self-heal ensure pass (maybe_ensure_pages,
 * on admin_init) that idempotently adopts/creates any creatable, option-bound
 * registry page — core's plus any an extension contributes through the
 * `titancart_registered_pages` filter — whose option has drifted or whose page
 * was deleted, so extension pages no longer need a per-feature creation
 * migration. See the method docblock for the scope/safety + drift-guard notes.
 *
 * @package TitanCart\Modules\Pages
 */

namespace TitanCart\Modules\Pages;

use TitanCart\REST\Pages\PagesController;

defined( 'ABSPATH' ) || exit;

class PagesModule {

	public static function init(): void {
		add_action( 'rest_api_init', [ self::class, 'register_routes' ] );
		add_action( 'admin_init', [ self::class, 'maybe_ensure_pages' ] );
	}

	public static function register_routes(): void {
		( new PagesController() )->register_routes();
	}

	/**
	 * Runtime self-heal for the option-bound registry pages (S345).
	 *
	 * On every admin page load, idempotently (re)assert every creatable,
	 * option-bound registry page — core's gift_cards/testimonials AND any page
	 * an extension contributes through the `titancart_registered_pages` filter
	 * (e.g. Wishlist+'s shared list). This replaces the need for each feature to
	 * ship its own one-off page-creation migration: activate an extension, or
	 * delete/unbind a managed page, and the next admin load adopts or recreates
	 * it and rebinds its option.
	 *
	 * SCOPE / SAFETY
	 * ──────────────
	 * - admin_init fires only in wp-admin, so this NEVER runs on a storefront or
	 *   checkout request. AJAX and REST requests (which also run in the admin
	 *   context) are excluded, and the pass is gated to manage_options users.
	 * - Only creatable + option-bound (type 'shortcode') pages are touched.
	 *   Structural pages (homepage/blog) are option-less and their reading-
	 *   settings binding stays owned by Migration 23; rewrite (virtual) pages are
	 *   never WP pages. Both are skipped.
	 * - Data is never deleted: deactivating an extension merely drops its
	 *   descriptor from the registry, so its page is left untouched (unmanaged),
	 *   not removed.
	 *
	 * DRIFT GUARD (cheap steady state)
	 * ────────────────────────────────
	 * resolve_page_id() returns immediately from the stored option when it points
	 * at a live (cached) page, so a healthy install performs only a couple of
	 * cached reads per bound page and NO writes. The heavier work — ensure()'s
	 * shortcode-content adoption scan or a wp_insert_post — runs only when a page
	 * is genuinely missing (host <= 0) or its option has drifted from the live
	 * host (bound !== host).
	 */
	public static function maybe_ensure_pages(): void {
		if ( wp_doing_ajax() ) {
			return;
		}
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return;
		}
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		foreach ( PageRegistry::all() as $descriptor ) {
			if ( ! PageRegistry::is_creatable( $descriptor ) || '' === $descriptor['option'] ) {
				continue;
			}

			$host  = PageRegistry::resolve_page_id( $descriptor );
			$bound = (int) get_option( $descriptor['option'] );

			if ( $host <= 0 || $bound !== $host ) {
				// Missing → adopt-or-create + bind; drifted → adopt + rebind.
				// ensure() is the single idempotent create/adopt path (S343).
				PageRegistry::ensure( $descriptor );
			}
		}
	}
}
