<?php
/**
 * Standalone HTML preview of a single Template Library entry.
 *
 * Renders a template's node tree through the normal renderer into a bare
 * document. Used for two things:
 *   1. the "Preview" button in the Template Library, and
 *   2. generating real thumbnail screenshots (see bin/shoot-thumbs.js), so
 *      cards show the actual template instead of a placeholder.
 *
 * @package Jalpi_Page_Builder
 */

defined( 'ABSPATH' ) || exit;

class Jalpi_Template_Preview {

	private static ?self $instance = null;

	const NONCE_ACTION = 'jalpi_template_preview';

	public static function instance(): self {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	private function __construct() {
		add_action( 'wp_ajax_jalpi_template_preview', [ $this, 'handle' ] );
	}

	/**
	 * Resolve a template id to its canvas data.
	 * Handles saved templates (numeric) and all virtual kit id shapes.
	 */
	private function resolve( string $id ): ?array {
		if ( 0 === strpos( $id, 'kit:' ) || 0 === strpos( $id, 'kitsec:' ) || 0 === strpos( $id, 'kitchrome:' ) ) {
			$item = Jalpi_Kit_Templates::find( $id );
			if ( null === $item ) {
				return null;
			}
			return [ 'data' => $item['data'] ?? [], 'type' => $item['type'] ?? 'page' ];
		}

		$post_id = absint( $id );
		if ( ! $post_id ) {
			return null;
		}
		$post = get_post( $post_id );
		if ( ! $post || Jalpi_Templates::CPT !== $post->post_type ) {
			return null;
		}
		return [
			'data' => json_decode( $post->post_content, true ) ?: [],
			'type' => get_post_meta( $post_id, '_jalpi_tpl_type', true ) ?: 'page',
		];
	}

	public function handle(): void {
		if ( ! check_ajax_referer( self::NONCE_ACTION, '_wpnonce', false ) ) {
			wp_die( esc_html__( 'Invalid nonce.', 'jalpi-page-builder' ), '', [ 'response' => 403 ] );
		}
		if ( ! current_user_can( 'edit_posts' ) ) {
			wp_die( esc_html__( 'Unauthorized.', 'jalpi-page-builder' ), '', [ 'response' => 403 ] );
		}

		// The id can contain ':' so it must not be run through sanitize_key.
		$raw      = isset( $_GET['id'] ) ? wp_unslash( $_GET['id'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated below
		$id       = is_string( $raw ) ? trim( $raw ) : '';
		if ( '' === $id || ! preg_match( '/^[A-Za-z0-9:_-]{1,120}$/', $id ) ) {
			wp_die( esc_html__( 'Invalid template id.', 'jalpi-page-builder' ), '', [ 'response' => 400 ] );
		}

		$resolved = $this->resolve( $id );
		if ( null === $resolved ) {
			wp_die( esc_html__( 'Template not found.', 'jalpi-page-builder' ), '', [ 'response' => 404 ] );
		}

		// A section template is a single node; wrap it so the renderer sees the
		// {version, sections} document shape it expects.
		$data = $resolved['data'];
		if ( 'section' === $resolved['type'] && isset( $data['type'] ) ) {
			$data = [ 'version' => 1, 'sections' => [ $data ] ];
		} elseif ( ! isset( $data['sections'] ) && is_array( $data ) ) {
			$data = [ 'version' => 1, 'sections' => array_values( $data ) ];
		}

		// Preview mode: elements that resolve site data show the template's own
		// demo content instead, so the preview represents the DESIGN rather than
		// the current site's menus. Reset afterwards — the renderer is a
		// singleton and a leaked flag would follow later renders.
		$renderer = Jalpi_Renderer::instance();
		$renderer->set_preview_mode( true );
		try {
			$html = $renderer->render_to_string( $data, 0, true );
		} finally {
			$renderer->set_preview_mode( false );
		}

		// Kit-scoped CSS keys off a body class; without it kit templates render
		// unstyled. Derive it from the id where possible.
		$kit_class = '';
		if ( preg_match( '/^kit(?:sec|chrome)?:([a-z0-9_-]+):/', $id, $m ) ) {
			$kit_class = ' jalpi-kit-' . sanitize_key( $m[1] );
		}

		nocache_headers();
		header( 'Content-Type: text/html; charset=utf-8' );
		header( 'X-Frame-Options: SAMEORIGIN' );

		// get_language_attributes() emits its own attribute markup (lang="…"
		// dir="…"), so it cannot be passed through esc_attr() without mangling
		// the quotes. Build the same thing from escaped parts instead.
		$lang_attr = get_bloginfo( 'language' );
		echo '<!doctype html><html lang="' . esc_attr( $lang_attr ) . '"'
			. ( is_rtl() ? ' dir="rtl"' : '' )
			. '><head><meta charset="utf-8">';
		echo '<meta name="viewport" content="width=1280">';
		echo '<title>' . esc_html__( 'Template preview', 'jalpi-page-builder' ) . '</title>';
		// Emit the styles the renderer queued (frontend.css + per-node CSS).
		ob_start();
		wp_print_styles();
		$styles = ob_get_clean();
		echo $styles; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- style tags built by WP
		echo '<style>html,body{margin:0;padding:0;background:#fff}</style>';
		echo '</head><body class="jalpi-theme-page jalpi-template-preview' . esc_attr( $kit_class ) . '">';
		echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- sanitized at save time by Jalpi_Sanitizer
		echo '</body></html>';
		exit;
	}

	/** URL for previewing a template (admin-ajax, nonce-protected). */
	public static function url( string $id ): string {
		return add_query_arg(
			[
				'action'   => 'jalpi_template_preview',
				'id'       => rawurlencode( $id ),
				'_wpnonce' => wp_create_nonce( self::NONCE_ACTION ),
			],
			admin_url( 'admin-ajax.php' )
		);
	}
}
