<?php
/**
 * [bw_search_icon] — Brentwood global header search.
 *
 * Outputs a magnifying-glass icon (matching brentwood.ca's header) that, when
 * clicked, expands a dropdown search panel with live results — mirroring the
 * Laravel site's global-search component. Drop the shortcode into a Kadence
 * header HTML element (or anywhere else):
 *
 *     [bw_search_icon]
 *
 * Optional attributes:
 *   placeholder  -> input placeholder (default "Search Brentwood…")
 *   label        -> accessible label for the toggle button (default "Search")
 *
 * Live results are fetched from the WordPress REST search endpoint and filtered
 * to user-facing content client-side (see assets/bw-search.js). Pressing Enter
 * submits to the standard WordPress search results page (?s=…).
 *
 * @package Kadence-Child
 */

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

/**
 * Register the front-end assets (enqueued on demand by the shortcode).
 */
function bw_search_register_assets() {
	$dir = get_stylesheet_directory();
	$uri = get_stylesheet_directory_uri();

	wp_register_style(
		'bw-search',
		$uri . '/assets/bw-search.css',
		array(),
		file_exists( $dir . '/assets/bw-search.css' ) ? filemtime( $dir . '/assets/bw-search.css' ) : null
	);
	wp_register_script(
		'bw-search',
		$uri . '/assets/bw-search.js',
		array(),
		file_exists( $dir . '/assets/bw-search.js' ) ? filemtime( $dir . '/assets/bw-search.js' ) : null,
		true
	);
	wp_register_style(
		'bw-searchpage',
		$uri . '/assets/bw-searchpage.css',
		array(),
		file_exists( $dir . '/assets/bw-searchpage.css' ) ? filemtime( $dir . '/assets/bw-searchpage.css' ) : null
	);
	wp_register_script(
		'bw-searchpage',
		$uri . '/assets/bw-searchpage.js',
		array(),
		file_exists( $dir . '/assets/bw-searchpage.js' ) ? filemtime( $dir . '/assets/bw-searchpage.js' ) : null,
		true
	);
}
// Registered on `init` (not just wp_enqueue_scripts) so the 'bw-search' style
// handle also exists in the block editor: the bw/search "Brentwood Search"
// block reuses this handle for its viewScript and as a style dependency, and
// block styles are loaded into the editor canvas too. Registration is cheap and
// idempotent; the actual enqueue still happens on demand (shortcode / template).
add_action( 'init', 'bw_search_register_assets' );

/**
 * Load the search-results assets on the search and 404 templates, which render
 * the Laravel-style results page (search.php / 404.php). The sidebar-filter JS
 * (facet auto-submit + per-page stepper) is only needed on the search results
 * page, but it's tiny so we load it on both.
 */
function bw_search_page_assets() {
	if ( is_search() || is_404() ) {
		wp_enqueue_style( 'bw-searchpage' );
		wp_enqueue_script( 'bw-searchpage' );
	}
}
add_action( 'wp_enqueue_scripts', 'bw_search_page_assets' );

/* --------------------------------------------------------------------------
 * Search results page — mirrors the Laravel global-search result rows
 * (type icon + title + highlighted preview + breadcrumb/date + thumbnail).
 * Rendered by the child-theme search.php / 404.php templates.
 * ------------------------------------------------------------------------ */

/**
 * Post types included in site search. Matches the Laravel global-search scope
 * (pages, blogs, livestreams, courses) and deliberately EXCLUDES `staff`,
 * which is noindex / excluded from search per Rian's note, plus all the
 * Kadence/WP internal types.
 *
 * @return string[]
 */
function bw_search_post_types() {
	return apply_filters( 'bw_search_post_types', array( 'post', 'page', 'livestream', 'course', 'landing' ) );
}

/**
 * Sidebar filter facets — mirrors the Laravel global-search type filter.
 *
 * Each facet groups one or more WordPress post types under a single
 * human-facing label/checkbox. Laravel also exposes "Announcements" and
 * "Photos" facets, but those content types don't exist in this WordPress
 * install yet, so they're omitted here (add them to this map once the
 * corresponding post types are created).
 *
 * Keyed by a stable slug used as the checkbox value (?type[]=<slug>).
 *
 * @return array<string,array{label:string,types:string[]}>
 */
function bw_search_facets() {
	return apply_filters(
		'bw_search_facets',
		array(
			'pages'       => array(
				'label' => 'Pages',
				'types' => array( 'page', 'landing' ),
			),
			'livestreams' => array(
				'label' => 'Livestreams',
				'types' => array( 'livestream' ),
			),
			'courses'     => array(
				'label' => 'Courses',
				'types' => array( 'course' ),
			),
			'blogs'       => array(
				'label' => 'Blogs',
				'types' => array( 'post' ),
			),
		)
	);
}

/**
 * The facet slugs currently selected via ?type[]=…. When the query string
 * carries no `type` param at all, every facet is considered selected (the
 * default "show everything" state). An explicit-but-empty selection also
 * falls back to all, so the page never shows zero facets / zero results by
 * accident.
 *
 * @return string[] Selected facet slugs (subset of bw_search_facets() keys).
 */
function bw_search_selected_facets() {
	$all = array_keys( bw_search_facets() );

	if ( ! isset( $_GET['type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only GET filter.
		return $all;
	}

	$requested = array_map( 'sanitize_key', (array) wp_unslash( $_GET['type'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
	$selected  = array_values( array_intersect( $all, $requested ) );

	return $selected ? $selected : $all;
}

/**
 * Post types implied by the current facet selection (flattened).
 *
 * @return string[]
 */
function bw_search_selected_post_types() {
	$facets = bw_search_facets();
	$types  = array();
	foreach ( bw_search_selected_facets() as $slug ) {
		if ( isset( $facets[ $slug ]['types'] ) ) {
			$types = array_merge( $types, $facets[ $slug ]['types'] );
		}
	}
	$types = array_values( array_unique( $types ) );

	return $types ? $types : bw_search_post_types();
}

/**
 * Results-per-page from the ?per_page=… stepper, clamped to a sane range.
 * Defaults to 10 (matching Laravel).
 *
 * @return int
 */
function bw_search_per_page() {
	$pp = isset( $_GET['per_page'] ) ? (int) $_GET['per_page'] : 10; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
	if ( $pp < 1 ) {
		$pp = 10;
	}
	return min( $pp, 100 );
}

/**
 * Restrict the main search query to the facet-selected post types and apply
 * the chosen per-page count.
 *
 * @param WP_Query $q The query.
 */
function bw_search_filter_query( $q ) {
	if ( is_admin() || ! $q->is_main_query() || ! $q->is_search() ) {
		return;
	}
	$q->set( 'post_type', bw_search_selected_post_types() );
	$q->set( 'posts_per_page', bw_search_per_page() );
}
add_action( 'pre_get_posts', 'bw_search_filter_query' );

/**
 * Render the search results sidebar — the "N Results" heading, the type
 * filter checkboxes, and the Per Page stepper. Mirrors the Laravel
 * global-search sidebar. The whole sidebar is one GET form so changing a
 * filter (auto-submitted by bw-searchpage.js, or via the no-JS Apply button)
 * reloads the results page with the new query string.
 *
 * @param string $terms Current search terms.
 * @param int    $found Total results for the current query.
 * @return string HTML.
 */
function bw_search_render_sidebar( $terms, $found ) {
	$facets   = bw_search_facets();
	$selected = bw_search_selected_facets();
	$per_page = bw_search_per_page();
	$action   = esc_url( home_url( '/' ) );

	ob_start();
	?>
	<aside class="bw-facets" aria-label="Filter search results">
		<form class="bw-facets__form" method="get" action="<?php echo $action; ?>">
			<input type="hidden" name="s" value="<?php echo esc_attr( $terms ); ?>" />

			<p class="bw-facets__count">
				<span class="bw-facets__count-num"><?php echo esc_html( number_format_i18n( $found ) ); ?></span>
				<span class="bw-facets__count-label"><?php echo esc_html( _n( 'Result', 'Results', $found, 'kadence-child' ) ); ?></span>
			</p>

			<ul class="bw-facets__list">
				<?php foreach ( $facets as $slug => $facet ) : ?>
					<li class="bw-facets__item">
						<label class="bw-facet">
							<input type="checkbox" class="bw-facet__input" name="type[]"
								value="<?php echo esc_attr( $slug ); ?>"
								<?php checked( in_array( $slug, $selected, true ) ); ?> />
							<span class="bw-facet__box" aria-hidden="true"></span>
							<span class="bw-facet__label"><?php echo esc_html( $facet['label'] ); ?></span>
						</label>
					</li>
				<?php endforeach; ?>
			</ul>

			<div class="bw-facets__perpage">
				<label class="bw-facets__perpage-label" for="bw-per-page"><?php esc_html_e( 'Per Page', 'kadence-child' ); ?></label>
				<div class="bw-stepper" data-bw-stepper>
					<input type="number" id="bw-per-page" class="bw-stepper__input" name="per_page"
						value="<?php echo esc_attr( (string) $per_page ); ?>" min="1" max="100" step="1" inputmode="numeric" />
					<button type="button" class="bw-stepper__btn" data-step="up" aria-label="<?php esc_attr_e( 'Increase results per page', 'kadence-child' ); ?>">+</button>
					<button type="button" class="bw-stepper__btn" data-step="down" aria-label="<?php esc_attr_e( 'Decrease results per page', 'kadence-child' ); ?>">&minus;</button>
				</div>
			</div>

			<button type="submit" class="bw-facets__apply"><?php esc_html_e( 'Apply filters', 'kadence-child' ); ?></button>
		</form>
	</aside>
	<?php
	return (string) ob_get_clean();
}

/**
 * Icon + human label for a post type's search row.
 *
 * @param string $post_type Post type slug.
 * @return array{0:string,1:string} [ icon name, label ].
 */
function bw_search_type_meta( $post_type ) {
	switch ( $post_type ) {
		case 'post':
			return array( 'blog', 'Blogs' );
		case 'livestream':
			return array( 'youtube', 'Livestreams' );
		case 'course':
			return array( 'file', 'Courses' );
		case 'page':
		case 'landing':
			return array( 'file', 'Pages' );
		default:
			$obj   = get_post_type_object( $post_type );
			$label = ( $obj && ! empty( $obj->labels->name ) ) ? $obj->labels->name : ucfirst( $post_type );
			return array( 'file', $label );
	}
}

/**
 * Breadcrumb string for a result, e.g. "Blogs › Grad Dress-Up Day"
 * (Laravel's url_text). Includes page ancestors for hierarchical types.
 *
 * @param WP_Post $post Post.
 * @return string
 */
function bw_search_breadcrumb( $post ) {
	list( , $label ) = bw_search_type_meta( $post->post_type );
	$parts           = array( $label );
	if ( is_post_type_hierarchical( $post->post_type ) ) {
		foreach ( array_reverse( get_post_ancestors( $post ) ) as $aid ) {
			$parts[] = wp_strip_all_tags( get_the_title( $aid ) );
		}
	}
	$parts[] = wp_strip_all_tags( get_the_title( $post ) );
	return implode( ' › ', $parts );
}

/**
 * Wrap matched search words (longer than 2 chars) in <strong>, on
 * already-escaped text. Returns HTML safe to echo.
 *
 * @param string $text  Plain text (will be escaped here).
 * @param string $terms Raw search terms.
 * @return string
 */
function bw_search_highlight( $text, $terms ) {
	$esc   = esc_html( $text );
	$words = array_filter(
		preg_split( '/\s+/', trim( (string) $terms ) ),
		function ( $w ) {
			return mb_strlen( $w ) > 2;
		}
	);
	foreach ( $words as $w ) {
		$esc = preg_replace( '/(' . preg_quote( $w, '/' ) . ')/iu', '<strong>$1</strong>', $esc );
	}
	return $esc;
}

/**
 * Build a ~240-char preview snippet centred on the first matched term, with
 * matches highlighted. Returns HTML.
 *
 * @param WP_Post $post  Post.
 * @param string  $terms Raw search terms.
 * @return string
 */
function bw_search_excerpt( $post, $terms ) {
	$raw = has_excerpt( $post ) ? $post->post_excerpt : $post->post_content;
	$raw = wp_strip_all_tags( strip_shortcodes( (string) $raw ) );
	$raw = trim( preg_replace( '/\s+/', ' ', $raw ) );
	if ( '' === $raw ) {
		return '';
	}

	$len   = 240;
	$pos   = false;
	$words = array_filter(
		preg_split( '/\s+/', trim( (string) $terms ) ),
		function ( $w ) {
			return mb_strlen( $w ) > 2;
		}
	);
	foreach ( $words as $w ) {
		$p = mb_stripos( $raw, $w );
		if ( false !== $p ) {
			$pos = $p;
			break;
		}
	}

	if ( false !== $pos && $pos > 60 ) {
		$snippet = '… ' . mb_substr( $raw, $pos - 60, $len );
	} else {
		$snippet = mb_substr( $raw, 0, $len );
	}
	if ( mb_strlen( $raw ) > mb_strlen( $snippet ) ) {
		$snippet = rtrim( $snippet ) . ' …';
	}
	return bw_search_highlight( $snippet, $terms );
}

/**
 * Render one search result row (mirrors the Laravel global-search-row layout).
 *
 * @param WP_Post $post  Post.
 * @param string  $terms Raw search terms.
 * @return string HTML.
 */
function bw_search_render_row( $post, $terms ) {
	list( $icon ) = bw_search_type_meta( $post->post_type );
	$url          = get_permalink( $post );
	$thumb        = has_post_thumbnail( $post ) ? get_the_post_thumbnail_url( $post, 'medium' ) : '';

	ob_start();
	?>
	<article class="bw-search-result">
		<div class="bw-search-result__row">
			<div class="bw-search-result__main">
				<span class="bw-search-result__icon"><?php echo bw_search_icon_svg( $icon ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
				<a class="bw-search-result__title" href="<?php echo esc_url( $url ); ?>"><?php echo esc_html( get_the_title( $post ) ); ?></a>
				<p class="bw-search-result__preview"><?php echo bw_search_excerpt( $post, $terms ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in helper ?></p>
				<div class="bw-search-result__meta">
					<span class="bw-search-result__crumb"><?php echo esc_html( bw_search_breadcrumb( $post ) ); ?></span>
					<span class="bw-search-result__date"><?php echo esc_html( get_the_date( 'M j, Y', $post ) ); ?></span>
				</div>
			</div>
			<?php if ( $thumb ) : ?>
				<div class="bw-search-result__photo">
					<a href="<?php echo esc_url( $url ); ?>" tabindex="-1" aria-hidden="true">
						<img src="<?php echo esc_url( $thumb ); ?>" alt="" loading="lazy" />
					</a>
				</div>
			<?php endif; ?>
		</div>
	</article>
	<?php
	return (string) ob_get_clean();
}

/**
 * The always-open inline search bar used at the top of the results / 404 page
 * (Laravel's inline global-search). Submits to the WP search results page.
 *
 * @param string $terms       Prefilled value.
 * @param string $placeholder Placeholder text.
 * @return string HTML.
 */
function bw_search_inline_form( $terms = '', $placeholder = 'Search Brentwood…' ) {
	ob_start();
	?>
	<form class="bw-searchform" role="search" method="get" action="<?php echo esc_url( home_url( '/' ) ); ?>">
		<div class="bw-searchform__field">
			<span class="bw-searchform__lead" aria-hidden="true"><?php echo bw_search_icon_svg( 'search' ); // phpcs:ignore ?></span>
			<input type="search" name="s" class="bw-searchform__input" value="<?php echo esc_attr( $terms ); ?>"
				placeholder="<?php echo esc_attr( $placeholder ); ?>" autocomplete="off" aria-label="Search Brentwood" autofocus />
		</div>
		<button type="submit" class="bw-searchform__submit" aria-label="Search"><?php echo bw_search_icon_svg( 'search' ); // phpcs:ignore ?></button>
	</form>
	<?php
	return (string) ob_get_clean();
}

/**
 * Inline SVG icons (FontAwesome solid/brand paths, matching brentwood.ca).
 *
 * Each entry is array( viewBox, path ). FontAwesome icons use different
 * viewBox widths, so the viewBox travels with the path.
 *
 * @param string $name search|times-circle|file|blog|bullhorn|youtube.
 * @return string SVG markup, or empty string for an unknown name.
 */
function bw_search_icon_svg( $name ) {
	$icons = array(
		// fa-magnifying-glass (fa-search) — same path as the Laravel header.
		'search'       => array( '0 0 512 512',
			'M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 '
			. '45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 '
			. '208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z' ),
		// fa-circle-xmark (fa-times-circle) — the clear button.
		'times-circle' => array( '0 0 512 512',
			'M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 '
			. '24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 '
			. '47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 '
			. '0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z' ),
		// fa-file-lines (file-alt) — pages/courses result rows.
		'file'         => array( '0 0 384 512',
			'M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 '
			. '64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM112 '
			. '256l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 '
			. '16-16zm0 64l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 '
			. '16-16zm0 64l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z' ),
		// fa-blog — blog/news result rows.
		'blog'         => array( '0 0 512 512',
			'M192 32c0 17.7 14.3 32 32 32c123.7 0 224 100.3 224 224c0 17.7 14.3 32 32 '
			. '32s32-14.3 32-32C512 132.3 379.7 0 224 0c-17.7 0-32 14.3-32 32zm0 96c0 17.7 14.3 '
			. '32 32 32c70.7 0 128 57.3 128 128c0 17.7 14.3 32 32 32s32-14.3 32-32c0-106-86-192-192-192c-17.7 '
			. '0-32 14.3-32 32zM96 144c0-26.5-21.5-48-48-48S0 117.5 0 144L0 368c0 79.5 64.5 144 144 '
			. '144s144-64.5 144-144s-64.5-144-144-144l-48 0 0 96 48 0c26.5 0 48 21.5 48 48s-21.5 48-48 '
			. '48s-48-21.5-48-48l0-224z' ),
		// fa-bullhorn — announcements.
		'bullhorn'     => array( '0 0 512 512',
			'M480 32c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9L381.7 53c-48 48-113.1 75-181 '
			. '75l-8.7 0-32 0-96 0c-35.3 0-64 28.7-64 64l0 96c0 35.3 28.7 64 64 64l0 128c0 17.7 '
			. '14.3 32 32 32l64 0c17.7 0 32-14.3 32-32l0-128 8.7 0c67.9 0 133 27 181 75l43.6 '
			. '43.6c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6l0-118.4c18.6-8.8 32-32.5 '
			. '32-60.4s-13.4-51.6-32-60.4L480 32zm-64 76.7L416 240l0 131.3C357.2 317.8 280.5 288 '
			. '200.7 288l-8.7 0 0-96 8.7 0c79.8 0 156.5-29.8 215.3-83.3z' ),
		// fab fa-youtube — livestreams.
		'youtube'      => array( '0 0 576 512',
			'M549.7 124.1c-6.3-23.7-24.8-42.3-48.3-48.6C458.8 64 288 64 288 64S117.2 64 74.6 '
			. '75.5c-23.5 6.3-42 24.9-48.3 48.6-11.4 42.9-11.4 132.3-11.4 132.3s0 89.4 11.4 '
			. '132.3c6.3 23.7 24.8 41.5 48.3 47.8C117.2 448 288 448 288 448s170.8 0 213.4-11.5c23.5-6.3 '
			. '42-24.1 48.3-47.8 11.4-42.9 11.4-132.3 11.4-132.3s0-89.4-11.4-132.3zm-317.5 '
			. '213.5V175.2l142.7 81.2-142.7 81.2z' ),
	);

	if ( empty( $icons[ $name ] ) ) {
		return '';
	}

	list( $viewbox, $path ) = $icons[ $name ];

	return '<svg class="bw-search__svg" aria-hidden="true" focusable="false" role="img" '
		. 'xmlns="http://www.w3.org/2000/svg" viewBox="' . esc_attr( $viewbox ) . '">'
		. '<path fill="currentColor" d="' . $path . '"></path></svg>';
}

/**
 * The [bw_search_icon] shortcode.
 *
 * @param array $atts Shortcode attributes.
 * @return string Widget markup.
 */
function bw_search_icon_shortcode( $atts ) {
	$atts = shortcode_atts(
		array(
			'placeholder' => 'Search Brentwood…',
			'label'       => 'Search',
		),
		$atts,
		'bw_search_icon'
	);

	wp_enqueue_style( 'bw-search' );
	wp_enqueue_script( 'bw-search' );

	$rest   = esc_url( rest_url( 'wp/v2/search' ) );
	$action = esc_url( home_url( '/' ) );
	$ph     = esc_attr( $atts['placeholder'] );
	$label  = esc_attr( $atts['label'] );

	// Toggle icon uses the dedicated --toggle-icon class for sizing; the lead
	// and clear icons inherit .bw-search__svg sizing from their wrappers.
	$toggle_svg = str_replace(
		'class="bw-search__svg"',
		'class="bw-search__svg bw-search__toggle-icon"',
		bw_search_icon_svg( 'search' )
	);

	ob_start();
	?>
	<div class="bw-search" data-bw-search data-rest="<?php echo $rest; ?>">
		<button type="button" class="bw-search__toggle cp" aria-label="<?php echo $label; ?>" aria-expanded="false">
			<?php echo $toggle_svg; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static SVG ?>
		</button>
		<div class="bw-search__panel" hidden>
			<div class="bw-search__inner">
				<form class="bw-search__bar" role="search" method="get" action="<?php echo $action; ?>">
					<div class="bw-search__field">
						<span class="bw-search__lead" aria-hidden="true"><?php echo bw_search_icon_svg( 'search' ); // phpcs:ignore ?></span>
						<input type="search" name="s" class="bw-search__input"
							placeholder="<?php echo $ph; ?>" autocomplete="off"
							aria-label="<?php echo $label; ?>" />
					</div>
					<button type="button" class="bw-search__clear" aria-label="Clear search">
						<?php echo bw_search_icon_svg( 'times-circle' ); // phpcs:ignore ?>
					</button>
				</form>
				<div class="bw-search__results" role="listbox" aria-live="polite"></div>
			</div>
		</div>
	</div>
	<?php
	return (string) ob_get_clean();
}
add_shortcode( 'bw_search_icon', 'bw_search_icon_shortcode' );
