<?php
/**
 * Templates REST API controller.
 *
 * Handles CRUD operations for reusable header/footer templates.
 *
 * @package Antimanual_Builder
 * @since   1.0.0
 */

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

/**
 * Class AMBLD_API_Templates_Api
 *
 * REST API endpoints for builder template management.
 *
 * @since 1.0.0
 */
class AMBLD_API_Templates_Api {

	use AMBLD_API_Builder_Meta_Trait;

	/**
	 * Option key storing the list cache version.
	 *
	 * @since 1.0.0
	 * @var   string
	 */
	const LIST_CACHE_VERSION_OPTION = 'ambld_templates_list_cache_version';

	/**
	 * Lifetime for cached list responses in seconds.
	 *
	 * @since 1.0.0
	 * @var   int
	 */
	const LIST_CACHE_TTL = 60;

	/**
	 * REST namespace.
	 *
	 * @since 1.0.0
	 * @var   string
	 */
	private $namespace = 'ambld/v1';

	/**
	 * Register routes.
	 *
	 * @since  1.0.0
	 * @return void
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/templates',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_templates' ),
					'permission_callback' => array( $this, 'can_edit_pages' ),
					'args'                => array(
						'kind'    => array(
							'default'           => '',
							'sanitize_callback' => 'sanitize_key',
						),
						'per_page' => array(
							'default'           => 20,
							'sanitize_callback' => 'absint',
						),
						'page'    => array(
							'default'           => 1,
							'sanitize_callback' => 'absint',
						),
						'status'  => array(
							'default'           => 'any',
							'sanitize_callback' => 'sanitize_text_field',
						),
						'search'  => array(
							'default'           => '',
							'sanitize_callback' => 'sanitize_text_field',
						),
						'orderby' => array(
							'default'           => 'modified',
							'sanitize_callback' => 'sanitize_text_field',
						),
						'order'   => array(
							'default'           => 'DESC',
							'sanitize_callback' => 'sanitize_text_field',
						),
						'context' => array(
							'default'           => 'list',
							'sanitize_callback' => 'sanitize_text_field',
						),
					),
				),
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_template' ),
					'permission_callback' => array( $this, 'can_edit_pages' ),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/templates/(?P<id>\d+)',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_template' ),
					'permission_callback' => array( $this, 'can_edit_specific_template' ),
				),
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_template' ),
					'permission_callback' => array( $this, 'can_edit_specific_template' ),
				),
				array(
					'methods'             => \WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_template' ),
					'permission_callback' => array( $this, 'can_delete_specific_template' ),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/templates/(?P<id>\d+)/duplicate',
			array(
				'methods'             => \WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'duplicate_template' ),
				'permission_callback' => array( $this, 'can_edit_specific_template' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/templates/(?P<id>\d+)/usage',
			array(
				'methods'             => \WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_template_usage' ),
				'permission_callback' => array( $this, 'can_edit_specific_template' ),
			)
		);
	}

	/**
	 * Permission check for list/create routes.
	 *
	 * @since  1.0.0
	 * @return bool
	 */
	public function can_edit_pages() {
		return current_user_can( 'edit_pages' );
	}

	/**
	 * Permission check for template edit routes.
	 *
	 * @since  1.0.0
	 * @param  WP_REST_Request $request Request object.
	 * @return true|WP_Error
	 */
	public function can_edit_specific_template( $request ) {
		$post_id = absint( $request->get_param( 'id' ) );
		if ( $post_id <= 0 || ! get_post( $post_id ) ) {
			return new \WP_Error( 'rest_post_not_found', __( 'Template not found.', 'antimanual-builder' ), array( 'status' => 404 ) );
		}
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return new \WP_Error( 'rest_forbidden', __( 'You do not have permission to edit this template.', 'antimanual-builder' ), array( 'status' => 403 ) );
		}
		return true;
	}

	/**
	 * Permission check for template delete routes.
	 *
	 * @since  1.0.0
	 * @param  WP_REST_Request $request Request object.
	 * @return true|WP_Error
	 */
	public function can_delete_specific_template( $request ) {
		$post_id = absint( $request->get_param( 'id' ) );
		if ( $post_id <= 0 || ! get_post( $post_id ) ) {
			return new \WP_Error( 'rest_post_not_found', __( 'Template not found.', 'antimanual-builder' ), array( 'status' => 404 ) );
		}
		if ( ! current_user_can( 'delete_post', $post_id ) ) {
			return new \WP_Error( 'rest_forbidden', __( 'You do not have permission to delete this template.', 'antimanual-builder' ), array( 'status' => 403 ) );
		}
		return true;
	}

	/**
	 * List templates.
	 *
	 * @since  1.0.0
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function get_templates( $request ) {
		$cached_response = $this->get_cached_list_response( $request );
		if ( null !== $cached_response ) {
			return new \WP_REST_Response( $cached_response, 200 );
		}

		$per_page = max( 1, min( 100, (int) $request->get_param( 'per_page' ) ) );
		$page     = max( 1, (int) $request->get_param( 'page' ) );
		$context  = $this->normalize_response_context( $request->get_param( 'context' ) );
		$kind     = $this->normalize_template_kind( $request->get_param( 'kind' ) );

		$args = array(
			'post_type'      => AMBLD_Post_Type_Builder_Template::POST_TYPE,
			'posts_per_page' => $per_page,
			'paged'          => $page,
			'post_status'    => $this->normalize_list_statuses( $request->get_param( 'status' ) ),
			'orderby'        => $this->normalize_list_orderby( $request->get_param( 'orderby' ) ),
			'order'          => $this->normalize_list_order( $request->get_param( 'order' ) ),
			'ignore_sticky_posts'    => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
			'cache_results'          => true,
		);

		if ( '' !== $kind ) {
			$args['meta_query'] = array(
				array(
					'key'   => '_ambld_template_kind',
					'value' => $kind,
				),
			);
		}

		$search = $request->get_param( 'search' );
		if ( ! empty( $search ) ) {
			$args['s'] = $search;
		}

		$query     = new \WP_Query( $args );
		$templates = array();
		foreach ( $query->posts as $post ) {
			$templates[] = $this->format_template( $post, $context );
		}

		$response = array(
			'items'      => $templates,
			'total'      => (int) $query->found_posts,
			'totalPages' => (int) $query->max_num_pages,
		);

		$this->set_cached_list_response( $request, $response );

		return new \WP_REST_Response( $response, 200 );
	}

	/**
	 * Get a template.
	 *
	 * @since  1.0.0
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_template( $request ) {
		$post = get_post( $request['id'] );

		if ( ! $post || AMBLD_Post_Type_Builder_Template::POST_TYPE !== $post->post_type ) {
			return new \WP_Error( 'not_found', __( 'Template not found.', 'antimanual-builder' ), array( 'status' => 404 ) );
		}

		if ( ! current_user_can( 'edit_post', $post->ID ) ) {
			return new \WP_Error( 'forbidden', __( 'You do not have permission to view this template.', 'antimanual-builder' ), array( 'status' => 403 ) );
		}

		return new \WP_REST_Response( $this->format_template( $post, 'detail' ), 200 );
	}

	/**
	 * Create a template.
	 *
	 * @since  1.0.0
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function create_template( $request ) {
		$params = $request->get_json_params();
		$params = is_array( $params ) ? $params : $request->get_params();
		$params = is_array( $params ) ? $params : array();

		$template_kind = $this->normalize_template_kind( isset( $params['templateKind'] ) ? $params['templateKind'] : '' );
		if ( '' === $template_kind ) {
			return new \WP_Error( 'invalid_template_kind', __( 'Template kind must be header or footer.', 'antimanual-builder' ), array( 'status' => 400 ) );
		}

		// Lift inline <style> blocks out of the rendered HTML and into
		// customCss so they survive the body-level sanitizer and the
		// asset pipeline can re-apply the styles the AI generated. `customJs`
		// is persisted by the meta trait and compiled into the JS asset.
		if ( isset( $params['renderedHtml'] ) ) {
			$extracted              = $this->extract_styles_from_html( $params['renderedHtml'] );
			$params['renderedHtml'] = $extracted['html'];
			$existing_css           = isset( $params['customCss'] ) ? (string) $params['customCss'] : '';
			$params['customCss']    = $this->merge_css( $existing_css, $extracted['css'] );
			$params['renderedHtml'] = $this->strip_inline_code_from_html( $params['renderedHtml'] );
		}

		$params['renderedHtml'] = isset( $params['renderedHtml'] ) ? $this->sanitize_builder_html_value( $params['renderedHtml'] ) : '';

		$post_id = wp_insert_post(
			array(
				'post_type'   => AMBLD_Post_Type_Builder_Template::POST_TYPE,
				'post_title'  => isset( $params['title'] ) ? sanitize_text_field( $params['title'] ) : __( 'Untitled Template', 'antimanual-builder' ),
				'post_status' => $this->normalize_builder_post_status( isset( $params['status'] ) ? $params['status'] : 'publish', 'publish' ),
				'post_author' => get_current_user_id(),
				'post_content' => $params['renderedHtml'],
			),
			true
		);

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

		$params['templateKind'] = $template_kind;
		$this->save_template_meta( $post_id, $params );

		do_action( 'ambld_page_saved', $post_id, get_post( $post_id ) );
		$this->bump_list_cache_version();

		$post = get_post( $post_id );
		return new \WP_REST_Response( $this->format_template( $post, 'detail' ), 201 );
	}

	/**
	 * Update a template.
	 *
	 * @since  1.0.0
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function update_template( $request ) {
		$post = get_post( $request['id'] );
		if ( ! $post || AMBLD_Post_Type_Builder_Template::POST_TYPE !== $post->post_type ) {
			return new \WP_Error( 'not_found', __( 'Template not found.', 'antimanual-builder' ), array( 'status' => 404 ) );
		}

		if ( ! current_user_can( 'edit_post', $post->ID ) ) {
			return new \WP_Error( 'forbidden', __( 'You do not have permission to edit this template.', 'antimanual-builder' ), array( 'status' => 403 ) );
		}

		$params = $request->get_json_params();
		$params = is_array( $params ) ? $params : $request->get_params();
		$params = is_array( $params ) ? $params : array();

		if ( isset( $params['renderedHtml'] ) ) {
			$extracted              = $this->extract_styles_from_html( $params['renderedHtml'] );
			$params['renderedHtml'] = $extracted['html'];
			$existing_css           = isset( $params['customCss'] ) ? (string) $params['customCss'] : '';
			$params['customCss']    = $this->merge_css( $existing_css, $extracted['css'] );
			$params['renderedHtml'] = $this->strip_inline_code_from_html( $params['renderedHtml'] );
			$params['renderedHtml'] = $this->sanitize_builder_html_value( $params['renderedHtml'] );
		}

		$post_data = array( 'ID' => $post->ID );
		if ( isset( $params['title'] ) ) {
			$post_data['post_title'] = sanitize_text_field( $params['title'] );
		}
		if ( isset( $params['status'] ) ) {
			$post_data['post_status'] = $this->normalize_builder_post_status( $params['status'], $post->post_status );
		}
		if ( isset( $params['renderedHtml'] ) ) {
			$post_data['post_content'] = $params['renderedHtml'];
		}

		$result = wp_update_post( $post_data, true );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		if ( isset( $params['templateKind'] ) ) {
			$template_kind = $this->normalize_template_kind( $params['templateKind'] );
			if ( '' === $template_kind ) {
				return new \WP_Error( 'invalid_template_kind', __( 'Template kind must be header or footer.', 'antimanual-builder' ), array( 'status' => 400 ) );
			}
			$params['templateKind'] = $template_kind;
		}

		$this->save_template_meta( $post->ID, $params );

		do_action( 'ambld_page_saved', $post->ID, get_post( $post->ID ) );
		$this->bump_list_cache_version();

		$post = get_post( $post->ID );
		return new \WP_REST_Response( $this->format_template( $post, 'detail' ), 200 );
	}

	/**
	 * Delete a template.
	 *
	 * @since  1.0.0
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete_template( $request ) {
		$post = get_post( $request['id'] );
		if ( ! $post || AMBLD_Post_Type_Builder_Template::POST_TYPE !== $post->post_type ) {
			return new \WP_Error( 'not_found', __( 'Template not found.', 'antimanual-builder' ), array( 'status' => 404 ) );
		}

		if ( ! current_user_can( 'delete_post', $post->ID ) ) {
			return new \WP_Error( 'forbidden', __( 'You do not have permission to delete this template.', 'antimanual-builder' ), array( 'status' => 403 ) );
		}

		// Capture impact before removing anything so the response can report
		// exactly what referenced this template.
		$usage = $this->count_template_usage( $post->ID );

		$result = wp_trash_post( $post->ID );
		if ( ! $result ) {
			return new \WP_Error( 'delete_failed', __( 'Failed to delete template.', 'antimanual-builder' ), array( 'status' => 500 ) );
		}

		// Repoint dangling references so live pages don't silently lose their
		// header/footer: global defaults reset to none, explicit page choices
		// fall back to "default".
		$this->cleanup_template_references( $post->ID );
		AMBLD_Render_Template_Resolver::flush_template_cache( $post->ID );

		$this->bump_list_cache_version();
		return new \WP_REST_Response(
			array(
				'deleted' => true,
				'usage'   => $usage,
			),
			200
		);
	}

	/**
	 * Report what currently references a template.
	 *
	 * @since  1.0.0
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_template_usage( $request ) {
		$post = get_post( $request['id'] );
		if ( ! $post || AMBLD_Post_Type_Builder_Template::POST_TYPE !== $post->post_type ) {
			return new \WP_Error( 'not_found', __( 'Template not found.', 'antimanual-builder' ), array( 'status' => 404 ) );
		}

		return new \WP_REST_Response( $this->count_template_usage( $post->ID ), 200 );
	}

	/**
	 * Count references to a template across pages and global design defaults.
	 *
	 * @since  1.0.0
	 * @param  int $template_id Template post ID.
	 * @return array{ pages:int, globalHeader:bool, globalFooter:bool }
	 */
	private function count_template_usage( $template_id ) {
		$template_id = absint( $template_id );
		$usage       = array(
			'pages'        => 0,
			'globalHeader' => false,
			'globalFooter' => false,
		);

		if ( $template_id <= 0 ) {
			return $usage;
		}

		$design_defaults = get_option( 'ambld_design_defaults', array() );
		if ( is_array( $design_defaults ) ) {
			$usage['globalHeader'] = isset( $design_defaults['headerTemplateId'] ) && absint( $design_defaults['headerTemplateId'] ) === $template_id;
			$usage['globalFooter'] = isset( $design_defaults['footerTemplateId'] ) && absint( $design_defaults['footerTemplateId'] ) === $template_id;
		}

		$usage['pages'] = count( $this->find_pages_referencing_template( $template_id ) );

		return $usage;
	}

	/**
	 * Find page IDs that explicitly reference a template in their settings.
	 *
	 * Uses a fast LIKE pre-filter on the JSON meta, then confirms each match by
	 * decoding the settings so unrelated numeric values can't produce false
	 * positives.
	 *
	 * @since  1.0.0
	 * @param  int $template_id Template post ID.
	 * @return int[]
	 */
	private function find_pages_referencing_template( $template_id ) {
		$template_id = absint( $template_id );
		if ( $template_id <= 0 ) {
			return array();
		}

		$query = new \WP_Query(
			array(
				'post_type'              => 'any',
				'post_status'            => 'any',
				'posts_per_page'         => 500,
				'fields'                 => 'ids',
				'no_found_rows'          => true,
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
				'meta_query'             => array(
					// The id may be JSON-encoded as a string ("123") or a
					// number (:123) depending on which UI attached the
					// template. Match both; page_references_template()
					// decode-confirms each hit, so the loose ':123' pattern
					// can't produce false positives in the result.
					'relation' => 'OR',
					array(
						'key'     => '_ambld_page_settings',
						'value'   => '"' . $template_id . '"',
						'compare' => 'LIKE',
					),
					array(
						'key'     => '_ambld_page_settings',
						'value'   => ':' . $template_id,
						'compare' => 'LIKE',
					),
				),
			)
		);

		$matches = array();
		foreach ( $query->posts as $page_id ) {
			if ( $this->page_references_template( $page_id, $template_id ) ) {
				$matches[] = (int) $page_id;
			}
		}

		return $matches;
	}

	/**
	 * Determine whether a page's settings explicitly select a template id.
	 *
	 * @since  1.0.0
	 * @param  int $page_id     Page post ID.
	 * @param  int $template_id Template post ID.
	 * @return bool
	 */
	private function page_references_template( $page_id, $template_id ) {
		$raw      = get_post_meta( $page_id, '_ambld_page_settings', true );
		$settings = is_string( $raw ) ? json_decode( $raw, true ) : $raw;
		if ( ! is_array( $settings ) ) {
			return false;
		}

		$target = (string) absint( $template_id );
		foreach ( array( 'headerTemplate', 'footerTemplate' ) as $key ) {
			if ( isset( $settings[ $key ] ) && (string) $settings[ $key ] === $target ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Repoint references to a deleted template so pages degrade gracefully.
	 *
	 * Global defaults pointing at the template are reset to none; explicit
	 * per-page choices fall back to "default" (inherit global). Without this a
	 * trashed template would silently vanish from live pages.
	 *
	 * @since  1.0.0
	 * @param  int $template_id Template post ID.
	 * @return void
	 */
	private function cleanup_template_references( $template_id ) {
		$template_id = absint( $template_id );
		if ( $template_id <= 0 ) {
			return;
		}

		$design_defaults = get_option( 'ambld_design_defaults', array() );
		if ( is_array( $design_defaults ) ) {
			$changed = false;
			if ( isset( $design_defaults['headerTemplateId'] ) && absint( $design_defaults['headerTemplateId'] ) === $template_id ) {
				$design_defaults['headerTemplateId'] = 0;
				$changed                             = true;
			}
			if ( isset( $design_defaults['footerTemplateId'] ) && absint( $design_defaults['footerTemplateId'] ) === $template_id ) {
				$design_defaults['footerTemplateId'] = 0;
				$changed                             = true;
			}
			if ( $changed ) {
				update_option( 'ambld_design_defaults', $design_defaults );
			}
		}

		$target = (string) $template_id;
		foreach ( $this->find_pages_referencing_template( $template_id ) as $page_id ) {
			$raw      = get_post_meta( $page_id, '_ambld_page_settings', true );
			$settings = is_string( $raw ) ? json_decode( $raw, true ) : $raw;
			if ( ! is_array( $settings ) ) {
				continue;
			}

			$changed = false;
			foreach ( array( 'headerTemplate', 'footerTemplate' ) as $key ) {
				if ( isset( $settings[ $key ] ) && (string) $settings[ $key ] === $target ) {
					$settings[ $key ] = 'default';
					$changed          = true;
				}
			}

			if ( $changed ) {
				update_post_meta( $page_id, '_ambld_page_settings', wp_slash( wp_json_encode( $settings ) ) );
			}
		}
	}

	/**
	 * Duplicate a template.
	 *
	 * @since  1.0.0
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function duplicate_template( $request ) {
		$source = get_post( $request['id'] );
		if ( ! $source || AMBLD_Post_Type_Builder_Template::POST_TYPE !== $source->post_type ) {
			return new \WP_Error( 'not_found', __( 'Source template not found.', 'antimanual-builder' ), array( 'status' => 404 ) );
		}

		if ( ! current_user_can( 'read_post', $source->ID ) ) {
			return new \WP_Error( 'forbidden', __( 'You do not have permission to duplicate this template.', 'antimanual-builder' ), array( 'status' => 403 ) );
		}

		$new_post_id = wp_insert_post(
			array(
				'post_type'   => AMBLD_Post_Type_Builder_Template::POST_TYPE,
				/* translators: %s: Original template title. */
				'post_title'  => sprintf( __( '%s (Copy)', 'antimanual-builder' ), $source->post_title ),
				'post_status' => 'publish',
				'post_author' => get_current_user_id(),
			),
			true
		);

		if ( is_wp_error( $new_post_id ) ) {
			return $new_post_id;
		}

		$meta_keys = array( '_ambld_builder_enabled', '_ambld_blocks', '_ambld_rendered_html', '_ambld_style_map', '_ambld_page_settings', '_ambld_template_kind' );
		foreach ( $meta_keys as $key ) {
			$value = get_post_meta( $source->ID, $key, true );
			if ( $value ) {
				update_post_meta( $new_post_id, $key, $value );
			}
		}

		AMBLD_Render_Asset_Generator::copy_source_files( $source->ID, $new_post_id );
		$this->bump_list_cache_version();

		$post = get_post( $new_post_id );
		return new \WP_REST_Response( $this->format_template( $post, 'detail' ), 201 );
	}

	/**
	 * Save template-specific meta.
	 *
	 * @since  1.0.0
	 * @param  int   $post_id Post ID.
	 * @param  array $params  Request params.
	 * @return void
	 */
	private function save_template_meta( $post_id, $params ) {
		$this->save_builder_meta( $post_id, $params );

		if ( isset( $params['pageSettings'] ) ) {
			$value = $this->sanitize_builder_page_settings_meta_value( $params['pageSettings'] );
			if ( is_array( $value ) || is_object( $value ) ) {
				$value = wp_json_encode( $value );
			}
			update_post_meta( $post_id, '_ambld_page_settings', is_string( $value ) ? wp_slash( $value ) : $value );
		}

		if ( isset( $params['templateKind'] ) ) {
			$template_kind = $this->normalize_template_kind( $params['templateKind'] );
			if ( '' !== $template_kind ) {
				update_post_meta( $post_id, '_ambld_template_kind', $template_kind );
			}
		}
	}

	/**
	 * Format template API response.
	 *
	 * @since  1.0.0
	 * @param  WP_Post $post    Post object.
	 * @param  string  $context Response context.
	 * @return array
	 */
	private function format_template( $post, $context = 'detail' ) {
		$template_kind = $this->normalize_template_kind( get_post_meta( $post->ID, '_ambld_template_kind', true ) );
		$edit_url      = admin_url(
			'admin.php?page=ambld-editor&entity_type=' . rawurlencode( AMBLD_Post_Type_Builder_Template::POST_TYPE ) . '&post_id=' . $post->ID
		);

		if ( 'list' === $context ) {
			return array(
				'id'           => $post->ID,
				'title'        => $post->post_title,
				'status'       => $post->post_status,
				'date'         => $post->post_date_gmt,
				'modified'     => $post->post_modified_gmt,
				'editUrl'      => $edit_url,
				'permalink'    => null,
				'templateKind' => $template_kind,
			);
		}

		return array(
			'id'            => $post->ID,
			'title'         => $post->post_title,
			'status'        => $post->post_status,
			'editUrl'       => $edit_url,
			'templateKind'  => $template_kind,
			'blocks'        => get_post_meta( $post->ID, '_ambld_blocks', true ),
			'renderedHtml'  => get_post_meta( $post->ID, '_ambld_rendered_html', true ),
			'customCss'     => (string) get_post_meta( $post->ID, '_ambld_custom_css', true ),
			'customJs'      => (string) get_post_meta( $post->ID, '_ambld_custom_js', true ),
			'styleMap'      => get_post_meta( $post->ID, '_ambld_style_map', true ),
			'pageSettings'  => get_post_meta( $post->ID, '_ambld_page_settings', true ),
			'thumbnail'     => get_the_post_thumbnail_url( $post->ID, 'medium' ),
			// GMT to match the list context, so the frontend can treat every
			// template timestamp as UTC when formatting relative times.
			'date'          => $post->post_date_gmt,
			'modified'      => $post->post_modified_gmt,
		);
	}

	/**
	 * Normalize template kind value.
	 *
	 * @since  1.0.0
	 * @param  mixed $kind Raw kind.
	 * @return string
	 */
	private function normalize_template_kind( $kind ) {
		$kind = sanitize_key( (string) $kind );

		return in_array( $kind, array( 'header', 'footer' ), true ) ? $kind : '';
	}

	/**
	 * Normalize response context.
	 *
	 * @since  1.0.0
	 * @param  string $context Raw context.
	 * @return string
	 */
	private function normalize_response_context( $context ) {
		return 'detail' === $context ? 'detail' : 'list';
	}

	/**
	 * Normalize list orderby.
	 *
	 * @since  1.0.0
	 * @param  string $orderby Raw value.
	 * @return string
	 */
	private function normalize_list_orderby( $orderby ) {
		$orderby = sanitize_key( (string) $orderby );
		if ( 'title' === $orderby ) {
			return 'title';
		}
		if ( 'date' === $orderby ) {
			return 'date';
		}
		return 'modified';
	}

	/**
	 * Normalize list order.
	 *
	 * @since  1.0.0
	 * @param  string $order Raw value.
	 * @return string
	 */
	private function normalize_list_order( $order ) {
		return 'ASC' === strtoupper( (string) $order ) ? 'ASC' : 'DESC';
	}

	/**
	 * Normalize the requested post status(es) into a WP_Query value.
	 *
	 * Accepts a single status, a comma-separated list (e.g. "publish,private"),
	 * or "any" / empty. Unknown statuses are dropped; an empty result falls back
	 * to "any" so callers never accidentally query nothing.
	 *
	 * @since  1.0.0
	 * @param  string $status Raw status param.
	 * @return string|string[]
	 */
	private function normalize_list_statuses( $status ) {
		$status = (string) $status;
		if ( '' === $status || 'any' === $status ) {
			return 'any';
		}

		$allowed = array( 'publish', 'private', 'draft', 'pending', 'future' );
		$parts   = array_filter( array_map( 'trim', explode( ',', $status ) ) );
		$valid   = array_values( array_intersect( $parts, $allowed ) );

		return empty( $valid ) ? 'any' : $valid;
	}

	/**
	 * Build transient list cache key.
	 *
	 * @since  1.0.0
	 * @param  WP_REST_Request $request Request object.
	 * @return string
	 */
	private function get_list_cache_key( $request ) {
		$version = (int) get_option( self::LIST_CACHE_VERSION_OPTION, 1 );
		return 'ambld_templates_list_' . $version . '_' . md5(
			wp_json_encode(
				array(
					'kind'     => $this->normalize_template_kind( $request->get_param( 'kind' ) ),
					'per_page' => (int) $request->get_param( 'per_page' ),
					'page'     => (int) $request->get_param( 'page' ),
					'status'   => (string) $request->get_param( 'status' ),
					'search'   => (string) $request->get_param( 'search' ),
					'orderby'  => $this->normalize_list_orderby( $request->get_param( 'orderby' ) ),
					'order'    => $this->normalize_list_order( $request->get_param( 'order' ) ),
					'context'  => $this->normalize_response_context( $request->get_param( 'context' ) ),
				)
			)
		);
	}

	/**
	 * Fetch cached list response.
	 *
	 * @since  1.0.0
	 * @param  WP_REST_Request $request Request object.
	 * @return array|null
	 */
	private function get_cached_list_response( $request ) {
		$cached = get_transient( $this->get_list_cache_key( $request ) );
		return is_array( $cached ) ? $cached : null;
	}

	/**
	 * Store cached list response.
	 *
	 * @since  1.0.0
	 * @param  WP_REST_Request $request Request object.
	 * @param  array           $response Response payload.
	 * @return void
	 */
	private function set_cached_list_response( $request, $response ) {
		set_transient( $this->get_list_cache_key( $request ), $response, self::LIST_CACHE_TTL );
	}

	/**
	 * Invalidate cached list responses.
	 *
	 * @since  1.0.0
	 * @return void
	 */
	private function bump_list_cache_version() {
		update_option( self::LIST_CACHE_VERSION_OPTION, time(), false );
	}

	/**
	 * Strip <style>, <script>, and inline event-handler attributes from HTML.
	 *
	 * @since 2.1.0
	 * @param string $html Raw HTML.
	 * @return string
	 */
	private function strip_inline_code_from_html( $html ) {
		if ( ! is_string( $html ) || '' === $html ) {
			return '';
		}

		$html = preg_replace( '/<style\b[^>]*>[\s\S]*?<\/style>/i', '', $html );
		$html = preg_replace( '/<script\b[^>]*>[\s\S]*?<\/script>/i', '', $html );
		$html = preg_replace( '/<script\b[^>]*\/?>/i', '', $html );
		$html = preg_replace( '/\son[a-z]+\s*=\s*"[^"]*"/i', '', $html );
		$html = preg_replace( "/\son[a-z]+\s*=\s*'[^']*'/i", '', $html );
		$html = AMBLD_API_Pages_Api::strip_remote_font_links( $html );

		return trim( (string) $html );
	}

	/**
	 * Extract <style> blocks out of the rendered HTML so they survive
	 * the body sanitizer and can be persisted as page-scoped CSS.
	 *
	 * @since 2.2.0
	 * @param string $html Raw rendered HTML.
	 * @return array { html: string, css: string }
	 */
	private function extract_styles_from_html( $html ) {
		if ( ! is_string( $html ) || '' === $html ) {
			return array( 'html' => '', 'css' => '' );
		}

		$css = '';
		if ( preg_match_all( '/<style[^>]*>([\s\S]*?)<\/style>/i', $html, $matches ) ) {
			foreach ( $matches[1] as $style_content ) {
				$css .= trim( $style_content ) . "\n";
			}
			$html = preg_replace( '/<style[^>]*>[\s\S]*?<\/style>/i', '', $html );
			$html = trim( (string) $html );
		}

		return array(
			'html' => (string) $html,
			'css'  => trim( $css ),
		);
	}

	/**
	 * Merge previously stored CSS with newly extracted CSS, preserving
	 * any non-AI rules carried over by the user.
	 *
	 * @since 2.2.0
	 * @param string $existing Existing CSS.
	 * @param string $new      Newly extracted CSS.
	 * @return string Merged CSS.
	 */
	private function merge_css( $existing, $new ) {
		$existing = is_string( $existing ) ? $existing : '';
		$new      = is_string( $new ) ? $new : '';

		if ( '' === $new ) {
			return $existing;
		}
		if ( '' === $existing ) {
			return $new;
		}

		$marker_start = '/* --- AI Generated Styles --- */';
		$marker_end   = '/* --- End AI Generated Styles --- */';

		if ( false !== strpos( $existing, $marker_start ) ) {
			$pattern  = '/' . preg_quote( $marker_start, '/' ) . '[\s\S]*?' . preg_quote( $marker_end, '/' ) . '/';
			$existing = trim( (string) preg_replace( $pattern, '', $existing ) );
		}

		$merged = $marker_start . "\n" . $new . "\n" . $marker_end;
		if ( '' !== $existing ) {
			$merged = $existing . "\n\n" . $merged;
		}

		return $merged;
	}
}
