<?php
/**
 * Shared query + markup helpers for the bw/youtube-list block.
 * Used by render.php (initial page render) AND the REST endpoint that powers
 * AJAX search/pagination — one source of truth for the row markup, mirroring
 * the brentwood/blog block's architecture.
 *
 * Data comes from the `livestream` post type (migrated from Laravel). Each
 * livestream carries ACF meta: ls_youtube_id, ls_start (UTC, Y-m-d H:i:s),
 * ls_length, ls_unlisted, and ls_no_recording (true = live-only, no recording).
 */
if ( ! defined( 'ABSPATH' ) ) { exit; }

/**
 * Normalize a raw config (block attributes or REST params) into safe values.
 */
function bw_yt_sanitize_config( $raw ) {
	$ids = function ( $v ) {
		if ( is_string( $v ) ) {
			$v = explode( ',', $v );
		}
		return array_values( array_filter( array_map( 'intval', (array) $v ) ) );
	};
	$scope = isset( $raw['scope'] ) ? (string) $raw['scope'] : 'completed';
	return array(
		'per_page'   => min( 50, max( 1, (int) ( $raw['per_page'] ?? 10 ) ) ),
		'scope'      => in_array( $scope, array( 'completed', 'upcoming', 'ongoing', 'all' ), true ) ? $scope : 'completed',
		'categories' => $ids( $raw['categories'] ?? array() ),
		// Livestream IDs to omit from the list (e.g. the stream you're already
		// watching on a single page — mirrors Laravel's exclude_id).
		'exclude'    => $ids( $raw['exclude'] ?? array() ),
		// When false (default) the list hides unlisted livestreams (ls_unlisted=1),
		// matching the old Laravel site. FILTER_VALIDATE_BOOLEAN handles both the
		// PHP bool from block attrs and the "true"/"false" string from REST params.
		'show_unlisted' => filter_var( $raw['show_unlisted'] ?? false, FILTER_VALIDATE_BOOLEAN ),
	);
}

/**
 * Build the WP_Query args for a livestream list page.
 * - Always excludes unlisted livestreams (ls_unlisted = 1).
 * - "completed": ls_start <= now (UTC), newest first.
 * - "upcoming":  ls_start >  now (UTC), soonest first.
 * - "ongoing":   started but not yet ended — ls_start <= now AND
 *                (ls_start + ls_length minutes) > now, soonest-started first.
 *                Mirrors the Laravel site's "Join Livestream" state
 *                (!has_completed && start_date is in the past). The end-time
 *                half can't be a meta_query (no arithmetic across two meta
 *                keys), so it's added by bw_yt_ongoing_clauses() in bw_yt_query();
 *                here the named clause only anchors the ls_start ordering.
 * - "all":       no date filter, newest first.
 * Ordering keys off the named ls_start meta clause.
 */
function bw_yt_query_args( $cfg, $page, $search ) {
	$now = current_time( 'mysql', true ); // UTC, matches ls_start storage.

	$meta_query = array( 'relation' => 'AND' );

	// Exclude unlisted (but keep posts that never set the flag) — unless the
	// block is configured to show unlisted livestreams too.
	if ( empty( $cfg['show_unlisted'] ) ) {
		$meta_query[] = array(
			'relation' => 'OR',
			array( 'key' => 'ls_unlisted', 'compare' => 'NOT EXISTS' ),
			array( 'key' => 'ls_unlisted', 'value' => '1', 'compare' => '!=' ),
		);
	}

	$order = 'DESC';
	if ( 'completed' === $cfg['scope'] ) {
		$meta_query['start_clause'] = array(
			'key' => 'ls_start', 'value' => $now, 'compare' => '<=', 'type' => 'DATETIME',
		);
		$order = 'DESC';
	} elseif ( 'upcoming' === $cfg['scope'] ) {
		$meta_query['start_clause'] = array(
			'key' => 'ls_start', 'value' => $now, 'compare' => '>', 'type' => 'DATETIME',
		);
		$order = 'ASC';
	} elseif ( 'ongoing' === $cfg['scope'] ) {
		// The full predicate (started AND not yet ended) is applied in
		// bw_yt_ongoing_clauses(); the named clause here just gives us an
		// ls_start join to order by. EXISTS matches any livestream with a start.
		$meta_query['start_clause'] = array(
			'key' => 'ls_start', 'compare' => 'EXISTS', 'type' => 'DATETIME',
		);
		$order = 'ASC'; // soonest-started first
	} else {
		// "all": still need the clause as an orderby anchor, but match any row
		// that has a start date.
		$meta_query['start_clause'] = array(
			'key' => 'ls_start', 'compare' => 'EXISTS', 'type' => 'DATETIME',
		);
		$order = 'DESC';
	}

	$args = array(
		'post_type'           => 'livestream',
		'post_status'         => 'publish',
		'ignore_sticky_posts' => true,
		'posts_per_page'      => $cfg['per_page'],
		'paged'               => max( 1, (int) $page ),
		'meta_query'          => $meta_query,
		'orderby'             => array( 'start_clause' => $order ),
	);
	if ( $cfg['categories'] ) {
		$args['tax_query'] = array( array(
			'taxonomy' => 'livestream_category',
			'field'    => 'term_id',
			'terms'    => $cfg['categories'],
		) );
	}
	if ( ! empty( $cfg['exclude'] ) ) {
		$args['post__not_in'] = $cfg['exclude'];
	}
	if ( '' !== $search ) {
		$args['s'] = $search;
	}
	return $args;
}

/**
 * posts_clauses filter for the "ongoing" scope: restrict to livestreams that
 * have started but not yet ended. Joins ls_start and ls_length directly (its
 * own aliases, independent of the meta_query join) and compares
 * (ls_start + ls_length minutes) against now (UTC). ls_length is cast to an
 * unsigned int, treating a missing/blank length as 0 (so a zero-length stream
 * is "ended" the instant it starts — matching Laravel's (int)$length behaviour).
 */
function bw_yt_ongoing_clauses( $clauses ) {
	global $wpdb;
	$now = current_time( 'mysql', true ); // UTC, matches ls_start storage.
	$clauses['join'] .= " INNER JOIN {$wpdb->postmeta} bwyt_s ON ( {$wpdb->posts}.ID = bwyt_s.post_id AND bwyt_s.meta_key = 'ls_start' )";
	$clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} bwyt_l ON ( {$wpdb->posts}.ID = bwyt_l.post_id AND bwyt_l.meta_key = 'ls_length' )";
	$clauses['where'] .= $wpdb->prepare(
		" AND bwyt_s.meta_value <= %s"
		. " AND DATE_ADD( bwyt_s.meta_value, INTERVAL CAST( COALESCE( NULLIF( bwyt_l.meta_value, '' ), '0' ) AS UNSIGNED ) MINUTE ) > %s",
		$now,
		$now
	);
	return $clauses;
}

/**
 * Run the configured query.
 *
 * @return array { posts: WP_Post[], total_pages: int, found: int }
 */
function bw_yt_query( $cfg, $page, $search ) {
	$page    = max( 1, (int) $page );
	$ongoing = ( 'ongoing' === $cfg['scope'] );
	if ( $ongoing ) {
		add_filter( 'posts_clauses', 'bw_yt_ongoing_clauses' );
	}
	$q = new WP_Query( bw_yt_query_args( $cfg, $page, $search ) );
	if ( $ongoing ) {
		remove_filter( 'posts_clauses', 'bw_yt_ongoing_clauses' );
	}
	return array(
		'posts'       => $q->posts,
		'total_pages' => max( 1, (int) $q->max_num_pages ),
		'found'       => (int) $q->found_posts,
	);
}

/** Inline SVG icons (FontAwesome paths, matching brentwood.ca). */
function bw_yt_icon( $name ) {
	switch ( $name ) {
		case 'youtube':
			return '<svg viewBox="0 0 576 512" aria-hidden="true" focusable="false" role="img" '
				. 'xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M549.655 124.083c-6.281'
				. '-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c'
				. '-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 '
				. '89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 '
				. '0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 '
				. '11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 '
				. '81.201z"></path></svg>';
		case 'calendar':
			return '<svg viewBox="0 0 448 512" aria-hidden="true" focusable="false" role="img" '
				. 'xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M96 32V64H48C21.5 64 0 '
				. '85.5 0 112v48H448V112c0-26.5-21.5-48-48-48H352V32c0-17.7-14.3-32-32-32s-32 14.3-32 32V64H160V32c0'
				. '-17.7-14.3-32-32-32S96 14.3 96 32zM448 192H0V464c0 26.5 21.5 48 48 48H400c26.5 0 48-21.5 '
				. '48-48V192z"></path></svg>';
		case 'search':
			return '<svg viewBox="0 0 512 512" aria-hidden="true" focusable="false" role="img" '
				. 'xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="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"></path></svg>';
	}
	return '';
}

/**
 * Format ls_start (stored UTC) into the site timezone, e.g.
 * "Friday June 12, 2026 1:15 pm PDT". Returns '' if no/invalid date.
 */
function bw_yt_format_date( $utc ) {
	$utc = trim( (string) $utc );
	if ( '' === $utc ) {
		return '';
	}
	try {
		$dt = new DateTime( $utc, new DateTimeZone( 'UTC' ) );
		$dt->setTimezone( wp_timezone() );
		return $dt->format( 'l F j, Y g:i a T' );
	} catch ( Exception $e ) {
		return '';
	}
}

/**
 * Fallback thumbnail for livestreams with no featured image. Built from the
 * uploads base URL (not hard-coded) so it resolves to the right domain on
 * demoing.info as well as the dev.brentwood.ca droplet.
 */
function bw_yt_default_thumb() {
	$up = wp_get_upload_dir();
	return trailingslashit( $up['baseurl'] ) . '2026/06/livestream_default.jpg';
}

/** ls_start (UTC) as an ISO-8601 UTC string for the front-end TZ switcher. */
function bw_yt_utc_iso( $utc ) {
	$utc = trim( (string) $utc );
	if ( '' === $utc ) {
		return '';
	}
	try {
		$dt = new DateTime( $utc, new DateTimeZone( 'UTC' ) );
		return $dt->format( 'c' );
	} catch ( Exception $e ) {
		return '';
	}
}

/**
 * One livestream row, matching the brentwood.ca /live list item: media column
 * (YouTube thumbnail + play overlay, links to the livestream page) and a body
 * column (title, date, "Watch Recording" link, recording-availability note).
 */
function bw_yt_render_row( $post ) {
	$url   = get_permalink( $post );
	$title = wptexturize( $post->post_title );
	$yt    = trim( (string) get_post_meta( $post->ID, 'ls_youtube_id', true ) );
	$thumb = get_the_post_thumbnail_url( $post, 'large' );
	if ( ! $thumb ) {
		$thumb = bw_yt_default_thumb();
	}
	$start = get_post_meta( $post->ID, 'ls_start', true );
	$date  = bw_yt_format_date( $start );
	$iso   = bw_yt_utc_iso( $start );
	$norec = (string) get_post_meta( $post->ID, 'ls_no_recording', true );

	$play = '<span class="bw-yt__play"><span class="bw-yt__play-bg" aria-hidden="true"></span>'
		. '<span class="bw-yt__play-icon">' . bw_yt_icon( 'youtube' ) . '</span></span>';

	// $thumb is always set now (featured image or the default), so always an <img>.
	$img = '<img src="' . esc_url( $thumb ) . '" alt="" loading="lazy">';

	// Media: clickable (with play overlay) only when a recording exists.
	if ( '' !== $yt ) {
		$media = '<a class="bw-yt__media" href="' . esc_url( $url ) . '" aria-label="'
			. esc_attr( sprintf( __( 'Watch: %s', 'kadence-child' ), $post->post_title ) ) . '">'
			. $play . $img . '</a>';
	} else {
		$media = '<div class="bw-yt__media bw-yt__media--noplay">' . $img . '</div>';
	}

	$note = '1' === $norec
		? __( 'This is a live broadcast only; no post-event recording will be made available.', 'kadence-child' )
		: __( 'The recording of this event will be posted after the event has concluded and has been processed for upload.', 'kadence-child' );

	$body  = '<div class="bw-yt__body">';
	$body .= '<h3 class="bw-yt__title">' . esc_html( $title ) . '</h3>';
	if ( '' !== $date ) {
		$body .= '<div class="bw-yt__date"><span class="bw-yt__date-icon">' . bw_yt_icon( 'calendar' ) . '</span>'
			. '<span class="bw-yt__date-text"' . ( '' !== $iso ? ' data-utc="' . esc_attr( $iso ) . '"' : '' ) . '>'
			. esc_html( $date ) . '</span></div>';
	}
	// Watch link (left) + recording note (right) share one row, like the live site.
	$body .= '<div class="bw-yt__actions">'
		. '<div class="bw-yt__links">'
		. '<a class="bw-yt__watch" href="' . esc_url( $url ) . '"><span class="bw-yt__watch-icon">'
		. bw_yt_icon( 'youtube' ) . '</span><span>' . esc_html__( 'Watch Recording', 'kadence-child' ) . '</span></a>'
		. '</div>'
		. '<div class="bw-yt__note">' . esc_html( $note ) . '</div>'
		. '</div>';
	$body .= '</div>';

	return '<div class="bw-yt__item">' . $media . $body . '</div>';
}

function bw_yt_render_rows( $posts ) {
	if ( ! $posts ) {
		return '<p class="bw-yt__empty">' . esc_html__( 'There are no Events to view.', 'kadence-child' ) . '</p>';
	}
	$out = '';
	foreach ( $posts as $post ) {
		$out .= bw_yt_render_row( $post );
	}
	return $out;
}

/** "208 Items" count label. Hidden entirely when there are no events. */
function bw_yt_render_count( $found ) {
	$found = max( 0, (int) $found );
	if ( 0 === $found ) {
		return '';
	}
	return esc_html( sprintf( _n( '%s Item', '%s Items', $found, 'kadence-child' ), number_format_i18n( $found ) ) );
}

/**
 * Numbered paginator (window of 5) with prev/next angle buttons — buttons,
 * not links, so clicking never changes the URL or scrolls the page. Mirrors
 * bw_blog_render_paginator.
 */
function bw_yt_render_paginator( $total_pages, $current ) {
	$total_pages = max( 1, (int) $total_pages );
	$current     = min( $total_pages, max( 1, (int) $current ) );
	if ( $total_pages <= 1 ) {
		return '';
	}
	$left  = '<svg viewBox="0 0 320 512" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">'
		. '<path fill="currentColor" d="M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 '
		. '32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8'
		. '-12.5-45.3 0l-160 160z"></path></svg>';
	$right = '<svg viewBox="0 0 320 512" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">'
		. '<path fill="currentColor" d="M278.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-160 160c-12.5 '
		. '12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L210.7 256 73.4 118.6c-12.5-12.5-12.5-32.8 0-45.3'
		. 's32.8-12.5 45.3 0l160 160z"></path></svg>';

	$win   = 5;
	$start = max( 1, $current - 2 );
	$end   = min( $total_pages, $start + $win - 1 );
	$start = max( 1, $end - $win + 1 );

	$h = '<button type="button" class="bw-yt__page bw-yt__page--prev" data-page="' . ( $current - 1 ) . '"'
		. ( $current <= 1 ? ' disabled' : '' ) . ' aria-label="' . esc_attr__( 'Previous page', 'kadence-child' ) . '">'
		. $left . '</button>';
	for ( $i = $start; $i <= $end; $i++ ) {
		$h .= '<button type="button" class="bw-yt__page' . ( $i === $current ? ' is-current' : '' ) . '" '
			. 'data-page="' . $i . '" aria-label="' . esc_attr( sprintf( __( 'Page %d', 'kadence-child' ), $i ) ) . '"'
			. ( $i === $current ? ' aria-current="true"' : '' ) . '>' . $i . '</button>';
	}
	$h .= '<button type="button" class="bw-yt__page bw-yt__page--next" data-page="' . ( $current + 1 ) . '"'
		. ( $current >= $total_pages ? ' disabled' : '' ) . ' aria-label="' . esc_attr__( 'Next page', 'kadence-child' ) . '">'
		. $right . '</button>';
	return $h;
}
