<?php
/**
 * Brentwood — "Auto Hide Old Blog Posts" (blog auto-archiving).
 *
 * Feature request from Rian's notes (PROJECT-BRIEF.md §6 "Blog auto-archiving",
 * Scope of Work). Behaviour, verbatim from the brief:
 *
 *   A configurable setting (N years) that filters blog posts older than the
 *   threshold OUT OF on-site post queries/listings, WHILE KEEPING THEM PUBLISHED
 *   AND REACHABLE VIA GOOGLE/SEARCH (not unpublished, not noindexed).
 *
 * So this is a *display* filter, not a status change. An old post:
 *   - drops out of on-site listings: archives (category/tag/author/date),
 *     on-site search, Kadence Posts / Query-Loop / Recent-Posts blocks, widgets;
 *   - STILL loads at its own /blogs/<slug>/ permalink (is_singular is exempt);
 *   - stays post_status = publish (we never touch status);
 *   - gets NO noindex added by us (Yoast still governs robots meta);
 *   - stays in the XML sitemap (Yoast builds it via direct SQL, not WP_Query;
 *     and we guard the WP-core sitemap too) so Google keeps crawling/ranking it.
 *
 * Feeds are intentionally left UNAFFECTED (owner decision, 2026-07-13): only the
 * on-screen listings are trimmed, consistent with "keep search-reachable".
 *
 * The threshold N (years) is set in Settings → Reading. 0 disables the feature.
 * Default 3 years.
 *
 * Mechanism: a pre_get_posts filter that adds a lower-bound date_query to any
 * front-end listing query over the 'post' type. No status writes, no cron, no
 * data migration — flip N and the visible archive changes on the next request.
 */

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

const BW_BLOG_ARCHIVE_OPTION  = 'bw_blog_archive_years';
const BW_BLOG_ARCHIVE_DEFAULT = 3;

/**
 * The effective threshold in whole years. 0 (or less) = feature off.
 *
 * Resolution order: BW_BLOG_ARCHIVE_YEARS constant (wp-config / theme) wins,
 * else the Settings → Reading option, else the default. Passed through a filter
 * so other code can override per-request if ever needed.
 *
 * @return int Years; < 1 means disabled.
 */
function bw_blog_archive_years() {
	if ( defined( 'BW_BLOG_ARCHIVE_YEARS' ) ) {
		$years = (int) BW_BLOG_ARCHIVE_YEARS;
	} else {
		$years = (int) get_option( BW_BLOG_ARCHIVE_OPTION, BW_BLOG_ARCHIVE_DEFAULT );
	}
	/** Filter the "hide older than N years" threshold. Return < 1 to disable. */
	return (int) apply_filters( 'bw_blog_archive_years', $years );
}

/* -------------------------------------------------------------------------
 * Settings → Reading: "Hide blog posts older than __ years"
 * ---------------------------------------------------------------------- */

/**
 * Register the option on the Reading settings screen and sanitise it.
 */
function bw_blog_archive_register_setting() {
	register_setting(
		'reading',
		BW_BLOG_ARCHIVE_OPTION,
		array(
			'type'              => 'integer',
			'sanitize_callback' => 'bw_blog_archive_sanitize',
			'default'           => BW_BLOG_ARCHIVE_DEFAULT,
			'show_in_rest'      => false,
		)
	);

	add_settings_field(
		BW_BLOG_ARCHIVE_OPTION,
		__( 'Auto-hide old blog posts', 'kadence-child' ),
		'bw_blog_archive_render_field',
		'reading',
		'default'
	);
}
add_action( 'admin_init', 'bw_blog_archive_register_setting' );

/**
 * Clamp to a sane non-negative whole number. 0 = disabled.
 *
 * @param mixed $value Raw submitted value.
 * @return int
 */
function bw_blog_archive_sanitize( $value ) {
	$value = (int) $value;
	if ( $value < 0 ) {
		$value = 0;
	}
	if ( $value > 100 ) {
		$value = 100; // guardrail against a stray huge number
	}
	return $value;
}

/**
 * Render the number field + help text on Settings → Reading.
 */
function bw_blog_archive_render_field() {
	$years    = (int) get_option( BW_BLOG_ARCHIVE_OPTION, BW_BLOG_ARCHIVE_DEFAULT );
	$is_const = defined( 'BW_BLOG_ARCHIVE_YEARS' );
	?>
	<label>
		<input type="number" min="0" max="100" step="1"
			name="<?php echo esc_attr( BW_BLOG_ARCHIVE_OPTION ); ?>"
			id="<?php echo esc_attr( BW_BLOG_ARCHIVE_OPTION ); ?>"
			value="<?php echo esc_attr( (string) $years ); ?>"
			class="small-text"<?php echo $is_const ? ' disabled' : ''; ?> />
		<?php esc_html_e( 'years', 'kadence-child' ); ?>
	</label>
	<p class="description">
		<?php
		esc_html_e(
			'Blog posts older than this are hidden from on-site listings — the blog archive, category/tag/author pages, on-site search, and Recent-Posts / Query-Loop blocks.',
			'kadence-child'
		);
		echo '<br>';
		echo '<strong>' . esc_html__( 'They stay published', 'kadence-child' ) . '</strong>';
		esc_html_e(
			' — each post still opens at its own link and remains indexable by Google (it stays in the XML sitemap). Set to ',
			'kadence-child'
		);
		echo '<code>0</code>';
		esc_html_e( ' to show all posts (feature off).', 'kadence-child' );
		?>
	</p>
	<?php
	if ( $is_const ) {
		echo '<p class="description"><em>'
			. esc_html__( 'This is currently fixed in code by the BW_BLOG_ARCHIVE_YEARS constant, so the field above is read-only.', 'kadence-child' )
			. '</em></p>';
	}
}

/* -------------------------------------------------------------------------
 * The listing filter
 * ---------------------------------------------------------------------- */

/**
 * Trim on-site listing queries to posts newer than the threshold.
 *
 * Exemptions (each old post must stay reachable/manageable):
 *   - wp-admin screens (editors manage the full set);
 *   - block-editor REST previews for users who can edit (WYSIWYG while building);
 *   - singular views (the post's own permalink must load);
 *   - feeds (owner decision: leave feeds complete);
 *   - the WP-core XML sitemap (keep old posts crawlable);
 *   - queries that explicitly name specific posts (curated "show these" blocks);
 *   - queries not over the 'post' type;
 *   - any query opting out via ->set( 'bw_include_archived', true ).
 *
 * @param WP_Query $q The query about to run.
 */
function bw_blog_archive_filter( $q ) {
	if ( ! ( $q instanceof WP_Query ) ) {
		return;
	}

	$years = bw_blog_archive_years();
	if ( $years < 1 ) {
		return; // disabled
	}

	// Never touch the admin, nor a block-editor preview for someone who can edit.
	if ( is_admin() ) {
		return;
	}
	if ( defined( 'REST_REQUEST' ) && REST_REQUEST && current_user_can( 'edit_posts' ) ) {
		return;
	}

	// The post's own page, and feeds, stay complete.
	if ( $q->is_singular() || $q->is_feed() ) {
		return;
	}

	// Keep the WP-core sitemap complete (Yoast's own sitemap already bypasses us).
	if ( ! empty( $GLOBALS['bw_in_core_sitemap'] ) ) {
		return;
	}

	// Per-query escape hatch.
	if ( $q->get( 'bw_include_archived' ) ) {
		return;
	}

	// Only trim queries that target blog posts, and only "listing" ones —
	// not a lookup of one specific/curated post.
	if ( ! bw_blog_archive_targets_posts( $q ) ) {
		return;
	}
	if ( bw_blog_archive_is_explicit_selection( $q ) ) {
		return;
	}

	// Add a lower bound: post_date must be within the last N years.
	$date_query   = (array) $q->get( 'date_query' );
	$date_query[] = array(
		'after'     => $years . ' years ago',
		'inclusive' => true,
		'column'    => 'post_date',
	);
	$q->set( 'date_query', $date_query );
}
add_action( 'pre_get_posts', 'bw_blog_archive_filter' );

/**
 * True if the query lists the 'post' type.
 *
 * Handles: explicit post_type='post' / arrays containing 'post' / 'any';
 * the main query in a post-listing context (archive/search/home/date);
 * and secondary queries with no post_type set (WP_Query defaults to 'post').
 *
 * @param WP_Query $q
 * @return bool
 */
function bw_blog_archive_targets_posts( $q ) {
	$pt = $q->get( 'post_type' );

	if ( empty( $pt ) ) {
		if ( $q->is_main_query() ) {
			// Only the post-listing main-query contexts (a page/front-page is singular
			// and already returned above; a taxonomy/CPT archive isn't "post").
			return $q->is_home()
				|| $q->is_search()
				|| $q->is_category()
				|| $q->is_tag()
				|| $q->is_author()
				|| $q->is_date();
		}
		// Secondary query with no post_type → WP_Query treats it as 'post'.
		return true;
	}

	if ( 'any' === $pt ) {
		return true;
	}

	return in_array( 'post', (array) $pt, true );
}

/**
 * True if the query hand-picks specific posts (by id, slug, or an explicit
 * post__in list) — a curated "feature these posts" block that a date filter
 * must not second-guess.
 *
 * @param WP_Query $q
 * @return bool
 */
function bw_blog_archive_is_explicit_selection( $q ) {
	if ( $q->get( 'p' ) || $q->get( 'name' ) || $q->get( 'page_id' ) || $q->get( 'pagename' ) ) {
		return true;
	}
	$in = $q->get( 'post__in' );
	return ! empty( $in );
}

/* -------------------------------------------------------------------------
 * WP-core XML sitemap guard (defense-in-depth; Yoast owns the live sitemap)
 * ---------------------------------------------------------------------- */

/**
 * Flag core-sitemap post queries so the listing filter leaves them complete,
 * keeping every old post crawlable even if the WP-core sitemap is ever used.
 *
 * @param array $args Query args for the sitemap provider's WP_Query.
 * @return array Unchanged.
 */
function bw_blog_archive_mark_core_sitemap( $args ) {
	$GLOBALS['bw_in_core_sitemap'] = true;
	return $args;
}
add_filter( 'wp_sitemaps_posts_query_args', 'bw_blog_archive_mark_core_sitemap' );
