<?php
/**
 * Brentwood migration blocks — registered from the child theme.
 * Loaded via require_once from functions.php.
 */
if ( ! defined( 'ABSPATH' ) ) { exit; }

/**
 * Livestream start-time timezone bridge.
 *
 * `ls_start` is stored in UTC — the live site stores UTC so its front-end
 * timezone switcher can convert event times to any viewer's zone. The ACF
 * date/time picker, however, is most intuitive when editors enter the event's
 * *local* Brentwood time. These filters bridge the two: the picker shows and
 * accepts local time, the database keeps UTC.
 *
 * Note: with the load filter, get_field('ls_start') returns LOCAL time. The
 * youtube-list block reads the raw meta (get_post_meta) and does its own
 * UTC->local conversion, so it is unaffected.
 */
function bw_ls_utc_to_local( $value ) {
	$value = trim( (string) $value );
	if ( '' === $value ) {
		return $value;
	}
	try {
		$dt = new DateTime( $value, new DateTimeZone( 'UTC' ) );
		$dt->setTimezone( wp_timezone() );
		return $dt->format( 'Y-m-d H:i:s' );
	} catch ( Exception $e ) {
		return $value;
	}
}

function bw_ls_local_to_utc( $value ) {
	$value = trim( (string) $value );
	if ( '' === $value ) {
		return $value;
	}
	$dt = DateTime::createFromFormat( 'Y-m-d H:i:s', $value, wp_timezone() );
	if ( ! $dt ) {
		$dt = DateTime::createFromFormat( 'Y-m-d H:i', $value, wp_timezone() );
	}
	if ( ! $dt ) {
		return $value;
	}
	$dt->setTimezone( new DateTimeZone( 'UTC' ) );
	return $dt->format( 'Y-m-d H:i:s' );
}

add_filter( 'acf/load_value/key=field_bwm_ls_start', function ( $value ) {
	return bw_ls_utc_to_local( $value );
} );
add_filter( 'acf/update_value/key=field_bwm_ls_start', function ( $value ) {
	return bw_ls_local_to_utc( $value );
} );

add_action( 'init', function () {
	$theme = get_stylesheet_directory();
	$uri   = get_stylesheet_directory_uri();

	// brentwood/media-grid — photo grid (server-rendered preview in editor)
	if ( file_exists( $theme . '/blocks/media-grid/block.json' ) ) {
		wp_register_script(
			'brentwood-media-grid-editor',
			$uri . '/blocks/media-grid/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-server-side-render', 'wp-block-editor' ),
			'1.0.0',
			true
		);
		register_block_type( $theme . '/blocks/media-grid' );
	}

	// brentwood/video-hero — full-bleed video + editable InnerBlocks text card
	if ( file_exists( $theme . '/blocks/video-hero/block.json' ) ) {
		wp_register_script(
			'brentwood-video-hero-editor',
			$uri . '/blocks/video-hero/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n' ),
			filemtime( $theme . '/blocks/video-hero/editor.js' ),
			true
		);
		// Carousel behaviour (autoplay + dots). Enqueued on demand by render.php
		// only when the block is in carousel mode.
		wp_register_script(
			'brentwood-video-hero-view',
			$uri . '/blocks/video-hero/frontend.js',
			array(),
			filemtime( $theme . '/blocks/video-hero/frontend.js' ),
			true
		);
		// Registered by handle (not block.json "file:") so the version is the
		// filemtime — block.json file styles get a constant ?ver= and stick in
		// the Cloudflare cache after edits.
		wp_register_style(
			'brentwood-video-hero',
			$uri . '/blocks/video-hero/style.css',
			array(),
			filemtime( $theme . '/blocks/video-hero/style.css' )
		);
		register_block_type( $theme . '/blocks/video-hero' );
	}

	// bw/video-text — video + text side-by-side with layout options
	if ( file_exists( $theme . '/blocks/video-text/block.json' ) ) {
		wp_register_script(
			'bw-video-text-editor',
			$uri . '/blocks/video-text/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-i18n' ),
			filemtime( $theme . '/blocks/video-text/editor.js' ),
			true
		);
		wp_register_script(
			'bw-video-text-view',
			$uri . '/blocks/video-text/frontend.js',
			array(),
			filemtime( $theme . '/blocks/video-text/frontend.js' ),
			true
		);
		// Styles registered by handle with a filemtime version so edits bust the
		// browser + Cloudflare cache automatically. block.json "file:" styles get
		// ver=false → a constant ?ver=<wp-version>, which sticks in the CDN cache
		// after edits (e.g. customizer-font follow-through never showing up).
		wp_register_style(
			'bw-video-text-style',
			$uri . '/blocks/video-text/style.css',
			array(),
			filemtime( $theme . '/blocks/video-text/style.css' )
		);
		wp_register_style(
			'bw-video-text-editor-style',
			$uri . '/blocks/video-text/editor.css',
			array(),
			filemtime( $theme . '/blocks/video-text/editor.css' )
		);
		register_block_type( $theme . '/blocks/video-text' );
	}

	// bw/interlinking — image tile + label pill linking to another page
	if ( file_exists( $theme . '/blocks/interlinking/block.json' ) ) {
		wp_register_script(
			'bw-interlinking-editor',
			$uri . '/blocks/interlinking/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-data', 'wp-i18n' ),
			filemtime( $theme . '/blocks/interlinking/editor.js' ),
			true
		);
		wp_register_script(
			'bw-interlinking-view',
			$uri . '/blocks/interlinking/view.js',
			array(),
			filemtime( $theme . '/blocks/interlinking/view.js' ),
			true
		);
		// Register the style handle explicitly with a filemtime version so CSS edits
		// cache-bust. Without this, block.json's "style" registers it with ver=false
		// (the static WP version), so style.css edits never reach the browser.
		wp_register_style(
			'bw-interlinking-style',
			$uri . '/blocks/interlinking/style.css',
			array(),
			filemtime( $theme . '/blocks/interlinking/style.css' )
		);
		register_block_type( $theme . '/blocks/interlinking' );
	}

	// bw/page-links — contextual sub-page navigation (parent + sibling tiles)
	if ( file_exists( $theme . '/blocks/page-links/block.json' ) ) {
		wp_register_script(
			'bw-page-links-editor',
			$uri . '/blocks/page-links/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-data', 'wp-i18n' ),
			filemtime( $theme . '/blocks/page-links/editor.js' ),
			true
		);
		register_block_type( $theme . '/blocks/page-links' );
	}

	// bw/table-link — editable link table / link grid.
	if ( file_exists( $theme . '/blocks/table-link/block.json' ) ) {
		wp_register_script(
			'bw-table-link-editor',
			$uri . '/blocks/table-link/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-data', 'wp-i18n' ),
			filemtime( $theme . '/blocks/table-link/editor.js' ),
			true
		);
		// Shared modal behaviour: clones a cell's <template> into one shared modal
		// on click. Table Link itself no longer uses it (the dynamic course grid
		// moved to bw/course-table), but course-table references this handle as its
		// viewScript, so it's registered here and loaded only where it's needed.
		wp_register_script(
			'bw-table-link-view',
			$uri . '/blocks/table-link/view.js',
			array(),
			filemtime( $theme . '/blocks/table-link/view.js' ),
			true
		);
		// Style registered by handle with a filemtime version so edits bust the
		// browser + Cloudflare cache automatically (block.json "file:" styles get
		// a constant ?ver= and stick in the cache after edits). Also carries the
		// shared grid + modal CSS that bw/course-table depends on.
		wp_register_style(
			'bw-table-link',
			$uri . '/blocks/table-link/style.css',
			array(),
			filemtime( $theme . '/blocks/table-link/style.css' )
		);
		register_block_type( $theme . '/blocks/table-link' );
	}

	// bw/library-search — GET form that sends a query to an external catalogue
	// (EBSCO Research by default). No view script: a plain HTML GET form.
	if ( file_exists( $theme . '/blocks/library-search/block.json' ) ) {
		wp_register_script(
			'bw-library-search-editor',
			$uri . '/blocks/library-search/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-i18n' ),
			filemtime( $theme . '/blocks/library-search/editor.js' ),
			true
		);
		// Explicit style handle with a filemtime version so edits bust the
		// browser + Cloudflare cache (block.json "file:" styles get a constant
		// ?ver= and stick in the cache after edits).
		wp_register_style(
			'bw-library-search',
			$uri . '/blocks/library-search/style.css',
			array(),
			filemtime( $theme . '/blocks/library-search/style.css' )
		);
		register_block_type( $theme . '/blocks/library-search' );
	}

	// bw/table-accordion — a link table whose cells expand a rich panel below the
	// table (accordion). The cells + panels come from bw/accordion-item children:
	// each child holds one cell label + its panel content (InnerBlocks). The
	// parent render.php draws the table and the panels and wires them by id; the
	// view script (bw-table-accordion-view) toggles them. Mirrors brentwood.ca's
	// course-selection expanders, self-contained in one block.
	if ( file_exists( $theme . '/blocks/table-accordion/block.json' ) ) {
		wp_register_script(
			'bw-table-accordion-editor',
			$uri . '/blocks/table-accordion/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-data', 'wp-i18n' ),
			filemtime( $theme . '/blocks/table-accordion/editor.js' ),
			true
		);
		// Child block: one accordion item (cell label + panel InnerBlocks). Only
		// valid inside bw/table-accordion; the parent renders it, so it needs no
		// render.php of its own.
		wp_register_script(
			'bw-accordion-item-editor',
			$uri . '/blocks/table-accordion/item-editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-i18n' ),
			filemtime( $theme . '/blocks/table-accordion/item-editor.js' ),
			true
		);
		wp_register_script(
			'bw-table-accordion-view',
			$uri . '/blocks/table-accordion/view.js',
			array(),
			filemtime( $theme . '/blocks/table-accordion/view.js' ),
			true
		);
		// Style registered by handle with a filemtime version so edits bust the
		// browser + Cloudflare cache automatically (block.json "file:" styles get
		// a constant ?ver= and stick in the cache after edits).
		wp_register_style(
			'bw-table-accordion',
			$uri . '/blocks/table-accordion/style.css',
			array(),
			filemtime( $theme . '/blocks/table-accordion/style.css' )
		);
		register_block_type( $theme . '/blocks/table-accordion' );      // parent
		register_block_type( $theme . '/blocks/table-accordion/item' ); // child (item)
	}

	// bw/faq-accordion — a two-column FAQ: a text card + question list beside a
	// large image, where clicking a question expands its answer in a panel below
	// the image. The intro text and each answer are empty inner-block areas; the
	// children are bw/faq-text (intro) and bw/faq-item (question + answer). The
	// parent render.php lays it all out and the view script (bw-faq-accordion-view)
	// toggles the panels. Mirrors brentwood.ca/admissions/financial-information.
	if ( file_exists( $theme . '/blocks/faq-accordion/block.json' ) ) {
		wp_register_script(
			'bw-faq-accordion-editor',
			$uri . '/blocks/faq-accordion/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-data', 'wp-i18n' ),
			filemtime( $theme . '/blocks/faq-accordion/editor.js' ),
			true
		);
		// Child block: the intro text area (its own InnerBlocks). Only valid inside
		// bw/faq-accordion; the parent renders it, so it needs no render.php.
		wp_register_script(
			'bw-faq-text-editor',
			$uri . '/blocks/faq-accordion/text-editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-block-editor' ),
			filemtime( $theme . '/blocks/faq-accordion/text-editor.js' ),
			true
		);
		// Child block: one question (attribute) + its answer (InnerBlocks).
		wp_register_script(
			'bw-faq-item-editor',
			$uri . '/blocks/faq-accordion/item-editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor' ),
			filemtime( $theme . '/blocks/faq-accordion/item-editor.js' ),
			true
		);
		wp_register_script(
			'bw-faq-accordion-view',
			$uri . '/blocks/faq-accordion/view.js',
			array(),
			filemtime( $theme . '/blocks/faq-accordion/view.js' ),
			true
		);
		// Style registered by handle with a filemtime version so edits bust the
		// browser + Cloudflare cache automatically.
		wp_register_style(
			'bw-faq-accordion',
			$uri . '/blocks/faq-accordion/style.css',
			array(),
			filemtime( $theme . '/blocks/faq-accordion/style.css' )
		);
		register_block_type( $theme . '/blocks/faq-accordion' );      // parent
		register_block_type( $theme . '/blocks/faq-accordion/text' ); // child (intro text)
		register_block_type( $theme . '/blocks/faq-accordion/item' ); // child (question + answer)
	}

	// bw/course-table — standalone dynamic course-link grid. Reuses the
	// table-link courses markup, CSS (bw-table-link) and modal JS
	// (bw-table-link-view); the query + modal body are shared via
	// inc/bw-course.php. Default on-click is the modal pop-up.
	if ( file_exists( $theme . '/blocks/course-table/block.json' ) ) {
		wp_register_script(
			'bw-course-table-editor',
			$uri . '/blocks/course-table/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-data', 'wp-i18n', 'wp-api-fetch', 'wp-server-side-render' ),
			filemtime( $theme . '/blocks/course-table/editor.js' ),
			true
		);
		// Thin wrapper style: depends on bw-table-link for the shared grid + modal
		// CSS. Registered by handle with a filemtime version so edits bust the
		// browser + Cloudflare cache automatically.
		wp_register_style(
			'bw-course-table',
			$uri . '/blocks/course-table/style.css',
			array( 'bw-table-link' ),
			filemtime( $theme . '/blocks/course-table/style.css' )
		);
		// viewScript is bw-table-link-view (registered above) — shared modal.
		register_block_type( $theme . '/blocks/course-table' );
	}

	// bw/hundred-grid — the Brentwood 100 mosaic. Renders every bw_hundred item
	// as a numbered tile (modal / inline / montage). Modal tiles reuse the shared
	// bw-table-link modal (view + CSS); a small view.js adds the per-visit shuffle
	// and #anchor deep-link. Query + modal body come from inc/bw-hundred.php.
	if ( file_exists( $theme . '/blocks/hundred-grid/block.json' ) ) {
		wp_register_script(
			'bw-hundred-grid-editor',
			$uri . '/blocks/hundred-grid/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-i18n', 'wp-server-side-render' ),
			filemtime( $theme . '/blocks/hundred-grid/editor.js' ),
			true
		);
		// Per-visit shuffle + deep-link. Depends on the shared modal view so its
		// trigger handlers are attached before we dispatch a deep-link click.
		wp_register_script(
			'bw-hundred-grid-view',
			$uri . '/blocks/hundred-grid/view.js',
			array( 'bw-table-link-view' ),
			filemtime( $theme . '/blocks/hundred-grid/view.js' ),
			true
		);
		wp_register_style(
			'bw-hundred-grid',
			$uri . '/blocks/hundred-grid/style.css',
			array( 'bw-table-link' ),
			filemtime( $theme . '/blocks/hundred-grid/style.css' )
		);
		register_block_type( $theme . '/blocks/hundred-grid' );
	}

	// bw/careers-grid — the Careers page listing. Renders every published
	// bw_career posting as: description, then the ACF "Open Message" beside an
	// "Apply Now" button ("Button Link"), then a divider — like the stacked list
	// on brentwood.ca/careers.
	if ( file_exists( $theme . '/blocks/careers-grid/block.json' ) ) {
		wp_register_script(
			'bw-careers-grid-editor',
			$uri . '/blocks/careers-grid/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-i18n', 'wp-server-side-render' ),
			filemtime( $theme . '/blocks/careers-grid/editor.js' ),
			true
		);
		wp_register_style(
			'bw-careers-grid',
			$uri . '/blocks/careers-grid/style.css',
			array(),
			filemtime( $theme . '/blocks/careers-grid/style.css' )
		);
		register_block_type( $theme . '/blocks/careers-grid' );
	}

	// bw/testimonial — quote testimonial with attribution
	if ( file_exists( $theme . '/blocks/testimonial/block.json' ) ) {
		wp_register_script(
			'bw-testimonial-editor',
			$uri . '/blocks/testimonial/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-i18n' ),
			filemtime( $theme . '/blocks/testimonial/editor.js' ),
			true
		);
		wp_localize_script( 'bw-testimonial-editor', 'bwTestimonial', array(
			'imgBase' => $uri . '/blocks/testimonial/images/',
		) );
		// Register the style by handle with a filemtime version so edits bust the
		// browser + Cloudflare cache automatically (block.json "file:" styles get
		// a constant ?ver= and stick in the cache after edits).
		wp_register_style(
			'bw-testimonial',
			$uri . '/blocks/testimonial/style.css',
			array(),
			filemtime( $theme . '/blocks/testimonial/style.css' )
		);
		register_block_type( $theme . '/blocks/testimonial' );
	}

	// brentwood/hero-card — the hero's white text card as editable InnerBlocks.
	// Style handle bw-page-hero is registered in inc/brentwood-hero.php.
	if ( file_exists( $theme . '/blocks/hero-card/block.json' ) ) {
		wp_register_script(
			'bw-hero-card-editor',
			$uri . '/blocks/hero-card/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-data', 'wp-i18n' ),
			filemtime( $theme . '/blocks/hero-card/editor.js' ),
			true
		);
		register_block_type( $theme . '/blocks/hero-card' );
	}

	// brentwood/blog — blog grid with AJAX search/pagination (Student Blog section)
	if ( file_exists( $theme . '/blocks/blog/block.json' ) ) {
		wp_register_script(
			'bw-blog-editor',
			$uri . '/blocks/blog/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n', 'wp-api-fetch', 'wp-server-side-render' ),
			filemtime( $theme . '/blocks/blog/editor.js' ),
			true
		);
		wp_register_script(
			'bw-blog-view',
			$uri . '/blocks/blog/view.js',
			array(),
			filemtime( $theme . '/blocks/blog/view.js' ),
			true
		);
		wp_register_style(
			'bw-blog',
			$uri . '/blocks/blog/style.css',
			array(),
			filemtime( $theme . '/blocks/blog/style.css' )
		);
		register_block_type( $theme . '/blocks/blog' );
	}

	// bw/youtube-list — livestream list with AJAX search/pagination (live page)
	if ( file_exists( $theme . '/blocks/youtube-list/block.json' ) ) {
		wp_register_script(
			'bw-youtube-list-editor',
			$uri . '/blocks/youtube-list/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n', 'wp-api-fetch', 'wp-server-side-render' ),
			filemtime( $theme . '/blocks/youtube-list/editor.js' ),
			true
		);
		wp_register_script(
			'bw-youtube-list-view',
			$uri . '/blocks/youtube-list/view.js',
			array(),
			filemtime( $theme . '/blocks/youtube-list/view.js' ),
			true
		);
		// Kadence only loads Open Sans 400 for body text, so a 600 request gets
		// faux-bolded (looks heavier than the live site, which serves Open Sans
		// Variable). Load the real 400/600 cuts so the date's semibold matches.
		wp_register_style(
			'bw-open-sans',
			'https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600&display=swap',
			array(),
			null
		);
		wp_register_style(
			'bw-youtube-list',
			$uri . '/blocks/youtube-list/style.css',
			array( 'bw-open-sans' ),
			filemtime( $theme . '/blocks/youtube-list/style.css' )
		);
		register_block_type( $theme . '/blocks/youtube-list' );
	}

	// bw/curve — curved divider (Left/Right) that bulges the section colour into
	// the adjacent image. Front-end assets (bw-curve.css/js) are registered in
	// inc/brentwood-curve.php and enqueued on render via bw_curve_markup().
	if ( file_exists( $theme . '/blocks/curve/block.json' ) ) {
		wp_register_script(
			'bw-curve-editor',
			$uri . '/blocks/curve/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-i18n' ),
			filemtime( $theme . '/blocks/curve/editor.js' ),
			true
		);
		register_block_type( $theme . '/blocks/curve' );
	}

	// brentwood/tagline — large body-font tagline (the live site's p.tag-line)
	// with text-colour + per-word highlight controls.
	if ( file_exists( $theme . '/blocks/tagline/block.json' ) ) {
		wp_register_script(
			'bw-tagline-editor',
			$uri . '/blocks/tagline/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-rich-text', 'wp-i18n' ),
			filemtime( $theme . '/blocks/tagline/editor.js' ),
			true
		);
		// Register the style by handle with a filemtime version so edits bust the
		// browser + Cloudflare cache automatically (block.json "file:" styles get
		// a constant ?ver= and stick in the cache after edits).
		wp_register_style(
			'bw-tagline',
			$uri . '/blocks/tagline/style.css',
			array(),
			filemtime( $theme . '/blocks/tagline/style.css' )
		);
		register_block_type( $theme . '/blocks/tagline' );
	}

	// bw/team-modal — staff photo grid that opens a profile modal on click
	// (mirrors brentwood.ca/academics/faculty). Dynamic: render.php draws the
	// grid from the Staff post type, view.js wires the shared modal.
	if ( file_exists( $theme . '/blocks/team-modal/block.json' ) ) {
		wp_register_script(
			'bw-team-modal-editor',
			$uri . '/blocks/team-modal/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-data', 'wp-i18n', 'wp-api-fetch', 'wp-server-side-render' ),
			filemtime( $theme . '/blocks/team-modal/editor.js' ),
			true
		);
		wp_register_script(
			'bw-team-modal-view',
			$uri . '/blocks/team-modal/view.js',
			array(),
			filemtime( $theme . '/blocks/team-modal/view.js' ),
			true
		);
		// Match the live faculty modal: Open Sans (body) + Oswald (the light
		// condensed heading face used for the staff name). Self-hosted on
		// brentwood.ca as the Variable cuts; the Google Fonts cuts are visually
		// identical. italic 400 covers the credentials line.
		wp_register_style(
			'bw-team-modal-fonts',
			'https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,600;0,700;1,400&family=Oswald:wght@300;400;500&display=swap',
			array(),
			null
		);
		// Style registered by handle with a filemtime version so edits bust the
		// browser + Cloudflare cache automatically (block.json "file:" styles get
		// a constant ?ver= and stick in the cache after edits).
		wp_register_style(
			'bw-team-modal',
			$uri . '/blocks/team-modal/style.css',
			array( 'bw-team-modal-fonts' ),
			filemtime( $theme . '/blocks/team-modal/style.css' )
		);
		register_block_type( $theme . '/blocks/team-modal' );
	}

	// bw/instructor — "Instructors" card listing department staff; reuses the
	// Team Modal profile modal (shared view script + modal/profile CSS).
	if ( file_exists( $theme . '/blocks/instructor/block.json' ) ) {
		wp_register_script(
			'bw-instructor-editor',
			$uri . '/blocks/instructor/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-i18n', 'wp-api-fetch', 'wp-server-side-render' ),
			filemtime( $theme . '/blocks/instructor/editor.js' ),
			true
		);
		// Card styling; depends on bw-team-modal for the shared modal + profile
		// CSS (and, transitively, the Open Sans + Oswald fonts).
		wp_register_style(
			'bw-instructor',
			$uri . '/blocks/instructor/style.css',
			array( 'bw-team-modal' ),
			filemtime( $theme . '/blocks/instructor/style.css' )
		);
		// viewScript is bw-team-modal-view (registered above) — shared modal.
		register_block_type( $theme . '/blocks/instructor' );
	}

	// bw/image-gallery — masonry image gallery with a full-screen lightbox
	// (mirrors brentwood.ca/arts/dance). Dynamic: render.php draws the masonry
	// from the images attribute, view.js wires the shared lightbox.
	if ( file_exists( $theme . '/blocks/image-gallery/block.json' ) ) {
		wp_register_script(
			'bw-image-gallery-editor',
			$uri . '/blocks/image-gallery/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-i18n' ),
			filemtime( $theme . '/blocks/image-gallery/editor.js' ),
			true
		);
		wp_register_script(
			'bw-image-gallery-view',
			$uri . '/blocks/image-gallery/view.js',
			array(),
			filemtime( $theme . '/blocks/image-gallery/view.js' ),
			true
		);
		// Editor-only chrome (per-box controls + the "+" add box).
		wp_register_style(
			'bw-image-gallery-editor-style',
			$uri . '/blocks/image-gallery/editor.css',
			array(),
			filemtime( $theme . '/blocks/image-gallery/editor.css' )
		);
		// Style registered by handle with a filemtime version so edits bust the
		// browser + Cloudflare cache automatically (block.json "file:" styles get
		// a constant ?ver= and stick in the cache after edits).
		wp_register_style(
			'bw-image-gallery',
			$uri . '/blocks/image-gallery/style.css',
			array(),
			filemtime( $theme . '/blocks/image-gallery/style.css' )
		);
		register_block_type( $theme . '/blocks/image-gallery' );
	}

	// bw/card-gallery — "Brentwood Images": a single image card (number badge +
	// label/chevron/dash + caption) that links somewhere or opens a popup whose
	// body is the block's own InnerBlocks (like bw/interlinking). Drop several into
	// a Row / Columns layout to build a grid. view.js wires the native <dialog>.
	if ( file_exists( $theme . '/blocks/card-gallery/block.json' ) ) {
		wp_register_script(
			'bw-card-gallery-editor',
			$uri . '/blocks/card-gallery/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-data', 'wp-i18n' ),
			filemtime( $theme . '/blocks/card-gallery/editor.js' ),
			true
		);
		wp_register_script(
			'bw-card-gallery-view',
			$uri . '/blocks/card-gallery/view.js',
			array(),
			filemtime( $theme . '/blocks/card-gallery/view.js' ),
			true
		);
		// Header font for the badge + label — the live site uses Oswald (variable);
		// the Google Fonts cuts are visually identical. Shared dependency for both
		// the front-end and editor styles so the card looks the same in both.
		wp_register_style(
			'bw-card-gallery-fonts',
			'https://fonts.googleapis.com/css2?family=Oswald:wght@400;500;600;700&display=swap',
			array(),
			null
		);
		// Editor-only chrome (inline editing + modal-edit overlay).
		wp_register_style(
			'bw-card-gallery-editor-style',
			$uri . '/blocks/card-gallery/editor.css',
			array( 'bw-card-gallery-fonts' ),
			filemtime( $theme . '/blocks/card-gallery/editor.css' )
		);
		// Style registered by handle with a filemtime version so edits bust the
		// browser + Cloudflare cache automatically (block.json "file:" styles get
		// a constant ?ver= and stick in the cache after edits).
		wp_register_style(
			'bw-card-gallery',
			$uri . '/blocks/card-gallery/style.css',
			array( 'bw-card-gallery-fonts' ),
			filemtime( $theme . '/blocks/card-gallery/style.css' )
		);
		register_block_type( $theme . '/blocks/card-gallery' );
	}

	// bw/timetable — the school's weekly timetable grid + Academic Block Rotation
	// (mirrors brentwood.ca/why-brentwood/unique-timetable). Phase 1: static. The
	// schedule is developer-maintained in blocks/timetable/grid.php; render.php
	// draws it server-side and the editor shows a ServerSideRender preview.
	if ( file_exists( $theme . '/blocks/timetable/block.json' ) ) {
		wp_register_script(
			'bw-timetable-editor',
			$uri . '/blocks/timetable/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-server-side-render' ),
			filemtime( $theme . '/blocks/timetable/editor.js' ),
			true
		);
		wp_register_script(
			'bw-timetable-view',
			$uri . '/blocks/timetable/view.js',
			array(),
			filemtime( $theme . '/blocks/timetable/view.js' ),
			true
		);
		wp_register_style(
			'bw-timetable',
			$uri . '/blocks/timetable/style.css',
			array(),
			filemtime( $theme . '/blocks/timetable/style.css' )
		);
		register_block_type( $theme . '/blocks/timetable' );
	}

	// bw/calendar — upcoming events aggregated from Brentwood's PUBLIC Google
	// Calendar ICS feeds (mirrors brentwood.ca/calendar). No API/credentials; the
	// data layer + caching live in inc/brentwood-calendar.php. render.php paints
	// the list server-side and the editor shows a ServerSideRender preview.
	if ( file_exists( $theme . '/blocks/calendar/block.json' ) ) {
		wp_register_script(
			'bw-calendar-editor',
			$uri . '/blocks/calendar/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-server-side-render' ),
			filemtime( $theme . '/blocks/calendar/editor.js' ),
			true
		);
		wp_register_script(
			'bw-calendar-view',
			$uri . '/blocks/calendar/view.js',
			array(),
			filemtime( $theme . '/blocks/calendar/view.js' ),
			true
		);
		wp_register_style(
			'bw-calendar',
			$uri . '/blocks/calendar/style.css',
			array(),
			filemtime( $theme . '/blocks/calendar/style.css' )
		);
		register_block_type( $theme . '/blocks/calendar' );
	}

	// bw/search — "Brentwood Search": a prominent, always-open site-search bar
	// (mirrors brentwood.ca/search). It REUSES the existing search mechanism:
	// the GET form submits to ?s= (rendered by the child theme's search.php
	// results template + inc/brentwood-search.php), and live "as you type"
	// results reuse the header dropdown's engine — the block's viewScript is the
	// shared 'bw-search' handle (assets/bw-search.js), run in inline mode via
	// the data-bw-inline attribute in render.php. The 'bw-search' script + style
	// handles are registered in inc/brentwood-search.php
	// (bw_search_register_assets); here we only add the editor script and the
	// block-specific style layer.
	if ( file_exists( $theme . '/blocks/search/block.json' ) ) {
		wp_register_script(
			'bw-search-block-editor',
			$uri . '/blocks/search/editor.js',
			array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-i18n' ),
			filemtime( $theme . '/blocks/search/editor.js' ),
			true
		);
		// Block-specific style layer; depends on the shared header-search CSS
		// (bw-search) for the input/results styling it extends. Registered by
		// handle with a filemtime version so edits bust the browser + CDN cache
		// automatically.
		wp_register_style(
			'bw-search-block',
			$uri . '/blocks/search/style.css',
			array( 'bw-search' ),
			filemtime( $theme . '/blocks/search/style.css' )
		);
		register_block_type( $theme . '/blocks/search' );
	}
} );

/**
 * Seed NEW pages with a Brentwood Hero Card block.
 *
 * The hero text card now lives only in this block (the old "Hero card text" /
 * "Show hero card" ACF fields were retired), so every new page opens with an
 * empty card ready to fill in — headings, buttons, tables, links, any block.
 * Pages that don't need a card just delete the block and build with Kadence
 * rows below. Only fires for brand-new pages opened in the editor (empty
 * content); existing pages are never touched, and the author can always
 * remove it.
 */
add_filter( 'default_content', function ( $content, $post ) {
	if ( $post instanceof WP_Post && 'page' === $post->post_type && '' === trim( (string) $content ) ) {
		return "<!-- wp:brentwood/hero-card -->\n"
			. "<!-- wp:paragraph {\"placeholder\":\"Hero card text… (headings, buttons, tables, links — any block)\"} -->\n"
			. "<p></p>\n"
			. "<!-- /wp:paragraph -->\n"
			. "<!-- /wp:brentwood/hero-card -->";
	}
	return $content;
}, 10, 2 );

/**
 * Expose the bw-dev "Override H1 page title" (post meta _bw_dev_title_override)
 * to the block editor so the hero-card block can both PREVIEW and EDIT it.
 *
 * The hero-card block's H1 is bound to this meta (a second "door" to the BW Dev
 * Page Title Override box; editor.js keeps the two in sync). It's a protected
 * key (underscore prefix), so REST read/write needs an explicit auth callback.
 *
 * sanitize_callback mirrors the bw-dev module's wp_kses allow-list. Critical:
 * the bw-dev `the_title` filter outputs the stored override RAW (it trusts the
 * value as cleaned-at-save), so anything written through REST/editPost MUST be
 * sanitized here to the same set, exactly as the classic meta box does on save.
 */
add_action( 'init', function () {
	$allowed = array(
		'em'     => array(),
		'strong' => array(),
		'i'      => array(),
		'b'      => array(),
		'br'     => array(),
		'span'   => array( 'class' => array() ),
	);
	foreach ( array( 'page', 'post' ) as $pt ) {
		register_post_meta( $pt, '_bw_dev_title_override', array(
			'type'              => 'string',
			'single'            => true,
			'show_in_rest'      => true,
			'sanitize_callback' => function ( $value ) use ( $allowed ) {
				return wp_kses( trim( (string) $value ), $allowed );
			},
			'auth_callback'     => function ( $allowed_cap, $meta_key, $post_id ) {
				return $post_id ? current_user_can( 'edit_post', (int) $post_id ) : current_user_can( 'edit_posts' );
			},
		) );
	}
} );

/**
 * REST endpoint behind the brentwood/blog block's AJAX search + pagination.
 * Public read-only data (published posts), same exposure as core /wp/v2/posts.
 */
add_action( 'rest_api_init', function () {
	register_rest_route( 'brentwood/v1', '/blog-cards', array(
		'methods'             => 'GET',
		'permission_callback' => '__return_true',
		'callback'            => function ( WP_REST_Request $req ) {
			require_once get_stylesheet_directory() . '/blocks/blog/helpers.php';
			$cfg  = bw_blog_sanitize_config( array(
				'per_page'   => $req->get_param( 'per_page' ),
				'offset'     => $req->get_param( 'offset' ),
				'categories' => $req->get_param( 'categories' ),
				'tags'       => $req->get_param( 'tags' ),
				'author_id'  => $req->get_param( 'author_id' ),
				'sticky'     => $req->get_param( 'sticky' ),
				'ratio'      => $req->get_param( 'ratio' ),
				'featured'   => $req->get_param( 'featured' ),
			) );
			$page = max( 1, min( 500, (int) $req->get_param( 'page' ) ) );
			$s    = mb_substr( sanitize_text_field( (string) $req->get_param( 's' ) ), 0, 100 );

			// Feature the newest post as a FIXED element; the grid paginates
			// the rest of the stream (offset + 1). The client swaps only the
			// grid on pagination and refreshes the feature only on a new search.
			if ( $cfg['featured'] ) {
				$featured_post      = bw_blog_featured_post( $cfg, $s );
				$featured_html      = $featured_post ? bw_blog_render_featured_card( $featured_post, $cfg['ratio'] ) : '';
				$grid_cfg           = $cfg;
				$grid_cfg['offset'] = $cfg['offset'] + 1;
				$result             = bw_blog_query( $grid_cfg, $page, $s );
			} else {
				$featured_html = '';
				$result        = bw_blog_query( $cfg, $page, $s );
			}

			return array(
				'featured'    => $featured_html,
				'cards'       => bw_blog_render_cards( $result['posts'], $cfg['ratio'] ),
				'paginator'   => bw_blog_render_paginator( $result['total_pages'], $page ),
				'total_pages' => $result['total_pages'],
				'found'       => $result['found'],
				'page'        => $page,
			);
		},
	) );

	// AJAX search + pagination behind the bw/youtube-list block. Public
	// read-only data (published, listed livestreams).
	register_rest_route( 'brentwood/v1', '/livestream-cards', array(
		'methods'             => 'GET',
		'permission_callback' => '__return_true',
		'callback'            => function ( WP_REST_Request $req ) {
			require_once get_stylesheet_directory() . '/blocks/youtube-list/helpers.php';
			$cfg  = bw_yt_sanitize_config( array(
				'per_page'   => $req->get_param( 'per_page' ),
				'scope'      => $req->get_param( 'scope' ),
				'categories' => $req->get_param( 'categories' ),
				'exclude'    => $req->get_param( 'exclude' ),
				'show_unlisted' => $req->get_param( 'show_unlisted' ),
			) );
			$page = max( 1, min( 1000, (int) $req->get_param( 'page' ) ) );
			$s    = mb_substr( sanitize_text_field( (string) $req->get_param( 's' ) ), 0, 100 );

			$result = bw_yt_query( $cfg, $page, $s );
			return array(
				'cards'       => bw_yt_render_rows( $result['posts'] ),
				'paginator'   => bw_yt_render_paginator( $result['total_pages'], $page ),
				'count'       => bw_yt_render_count( $result['found'] ),
				'total_pages' => $result['total_pages'],
				'found'       => $result['found'],
				'page'        => $page,
			);
		},
	) );

} );
