<?php
/**
 * Workshop status — derive Past / Upcoming from the event date.
 *
 * Previously the `workshop-status` taxonomy was tagged by hand, which drifted:
 * most workshops ended up with no term at all, so an "upcoming" list could not
 * be built without wrongly showing untagged past events.
 *
 * Here the DATE is the source of truth and the taxonomy term is a derived value
 * kept in sync automatically. Editors only ever type the date they already type.
 *
 * Flow:  display_date (free text)  ->  _aisv_event_end (sortable Ymd)  ->  term
 *
 * A date that cannot be understood is FLAGGED (admin notice) and the workshop's
 * existing term is left alone — it is never silently guessed.
 *
 * @package kadence-child
 */

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

define( 'AISV_WS_TAX', 'workshop-status' );
define( 'AISV_WS_META_END', '_aisv_event_end' );
define( 'AISV_WS_META_UNPARSED', '_aisv_event_date_unparsed' );
define( 'AISV_WS_META_SORT', '_aisv_sort_key' );

/**
 * Parse free-text event dates to the event's END date as a sortable Ymd string.
 *
 * Handles the formats actually in use on this site:
 *   "September 8, 2026"      -> 20260908
 *   "February 18th, 2026"    -> 20260218   (ordinal suffix)
 *   "November 1st. 2025"     -> 20251101   (ordinal + stray period)
 *   "Jun 3-4, 2026"          -> 20260604   (range, en-dash; END of range)
 *   "March 12-14, 2025"      -> 20250314   (range)
 *   "October 2025"           -> 20251031   (month only -> last day of month)
 *
 * The END of a range/month is used so an event stays "upcoming" until the day
 * it actually finishes.
 *
 * @param string $text Raw display date.
 * @return string|false 'Ymd' on success, false if it cannot be understood.
 */
function aisv_parse_event_end_date( $text ) {
	$text = (string) $text;
	if ( '' === trim( $text ) ) {
		return false;
	}

	// Normalise: lowercase, strip tags/entities, unify dash characters.
	$s = strtolower( wp_strip_all_tags( html_entity_decode( $text, ENT_QUOTES, 'UTF-8' ) ) );
	$s = str_replace( array( "\xe2\x80\x93", "\xe2\x80\x94", "\xe2\x80\x92" ), '-', $s ); // en/em/figure dash.

	// Drop ordinal suffixes: 1st, 2nd, 3rd, 18th -> 1, 2, 3, 18.
	$s = preg_replace( '/(\d+)\s*(st|nd|rd|th)\b/', '$1', $s );

	// Replace every separator with a space so stray commas/periods cannot glue tokens.
	$s = preg_replace( '/[^a-z0-9]+/', ' ', $s );
	$s = trim( preg_replace( '/\s+/', ' ', $s ) );

	// Year (required).
	if ( ! preg_match( '/\b((?:19|20)\d{2})\b/', $s, $ym ) ) {
		return false;
	}
	$year = (int) $ym[1];
	// Remove the year so it can never be mistaken for a day number.
	$s = trim( preg_replace( '/\b' . preg_quote( $ym[1], '/' ) . '\b/', ' ', $s ) );

	// Month (required).
	$months = array(
		'january' => 1, 'jan' => 1, 'february' => 2, 'feb' => 2, 'march' => 3, 'mar' => 3,
		'april' => 4, 'apr' => 4, 'may' => 5, 'june' => 6, 'jun' => 6, 'july' => 7, 'jul' => 7,
		'august' => 8, 'aug' => 8, 'september' => 9, 'sep' => 9, 'sept' => 9, 'october' => 10,
		'oct' => 10, 'november' => 11, 'nov' => 11, 'december' => 12, 'dec' => 12,
	);
	$month = 0;
	foreach ( $months as $name => $num ) {
		if ( preg_match( '/\b' . $name . '\b/', $s ) ) {
			$month = $num;
			break; // Longest names are listed first within each month.
		}
	}
	if ( ! $month ) {
		return false;
	}

	$last_day = (int) gmdate( 't', gmmktime( 0, 0, 0, $month, 1, $year ) );

	// Day(s): take the largest plausible day number => the END of any range.
	$day = 0;
	if ( preg_match_all( '/\b(\d{1,2})\b/', $s, $dm ) ) {
		foreach ( $dm[1] as $candidate ) {
			$candidate = (int) $candidate;
			if ( $candidate >= 1 && $candidate <= 31 && $candidate > $day ) {
				$day = $candidate;
			}
		}
	}
	// Month-only ("October 2025") -> treat as the last day of that month.
	if ( ! $day || $day > $last_day ) {
		$day = $last_day;
	}

	return sprintf( '%04d%02d%02d', $year, $month, $day );
}

/**
 * Recompute one workshop's derived end-date and status term.
 *
 * @param int $post_id Workshop post ID.
 * @return string|false The slug applied ('past'|'upcoming'), or false if unparseable.
 */
function aisv_sync_workshop_status( $post_id ) {
	$post_id = (int) $post_id;
	if ( ! $post_id || 'workshop' !== get_post_type( $post_id ) ) {
		return false;
	}

	$end = aisv_parse_event_end_date( get_post_meta( $post_id, 'display_date', true ) );

	if ( ! $end ) {
		// Flag it for the editor; never guess, never silently re-tag.
		update_post_meta( $post_id, AISV_WS_META_UNPARSED, '1' );
		delete_post_meta( $post_id, AISV_WS_META_END );
		return false;
	}

	delete_post_meta( $post_id, AISV_WS_META_UNPARSED );
	update_post_meta( $post_id, AISV_WS_META_END, $end );

	$slug = ( $end >= current_time( 'Ymd' ) ) ? 'upcoming' : 'past';
	$term = get_term_by( 'slug', $slug, AISV_WS_TAX );
	if ( $term && ! is_wp_error( $term ) ) {
		wp_set_object_terms( $post_id, array( (int) $term->term_id ), AISV_WS_TAX, false );
	}

	/*
	 * One ascending sort key so a single combined list can order correctly:
	 *   upcoming, soonest first   -> "A20261112"
	 *   then past, most recent    -> "B" . (99999999 - Ymd), zero padded
	 * Sorting ascending on this gives exactly the order an events page wants.
	 */
	$sort = ( 'upcoming' === $slug )
		? 'A' . $end
		: 'B' . str_pad( (string) ( 99999999 - (int) $end ), 8, '0', STR_PAD_LEFT );
	update_post_meta( $post_id, AISV_WS_META_SORT, $sort );

	return $slug;
}

/**
 * Recompute every workshop. Safe to run repeatedly.
 *
 * @return array Counts: past, upcoming, unparseable.
 */
function aisv_sync_all_workshop_statuses() {
	$ids = get_posts(
		array(
			'post_type'        => 'workshop',
			'post_status'      => array( 'publish', 'future', 'draft', 'private' ),
			'numberposts'      => -1,
			'fields'           => 'ids',
			'suppress_filters' => true,
		)
	);

	$out = array( 'past' => 0, 'upcoming' => 0, 'unparseable' => 0 );
	foreach ( $ids as $id ) {
		$slug = aisv_sync_workshop_status( $id );
		if ( $slug ) {
			$out[ $slug ]++;
		} else {
			$out['unparseable']++;
		}
	}
	return $out;
}

/* -------------------------------------------------------------------------
 * Keeping it in sync
 * ---------------------------------------------------------------------- */

// When a workshop is saved. ACF writes its fields on acf/save_post, so run after it.
add_action( 'acf/save_post', 'aisv_sync_workshop_status', 20, 1 );
add_action( 'save_post_workshop', 'aisv_sync_workshop_status', 20, 1 );

// Once a day, so an event flips to Past the day after it finishes.
add_action( 'init', function () {
	if ( ! wp_next_scheduled( 'aisv_workshop_status_daily' ) ) {
		wp_schedule_event( time() + 300, 'daily', 'aisv_workshop_status_daily' );
	}
} );
add_action( 'aisv_workshop_status_daily', 'aisv_sync_all_workshop_statuses' );

/**
 * Safety net: wp-cron on this site has been unreliable, and a missed run would
 * leave a finished event sitting in "Upcoming". Re-check once a day on an admin
 * page load too. Admin-only and transient-gated, so visitors never pay for it.
 */
add_action( 'admin_init', function () {
	if ( get_transient( 'aisv_ws_synced_today' ) ) {
		return;
	}
	set_transient( 'aisv_ws_synced_today', 1, DAY_IN_SECONDS );
	aisv_sync_all_workshop_statuses();
} );

/**
 * Tell the editor when a date could not be understood, rather than failing quietly.
 */
add_action( 'admin_notices', function () {
	if ( ! current_user_can( 'edit_posts' ) ) {
		return;
	}
	$flagged = get_posts(
		array(
			'post_type'   => 'workshop',
			'post_status' => array( 'publish', 'future', 'draft', 'private' ),
			'numberposts' => 20,
			'meta_key'    => AISV_WS_META_UNPARSED,
			'meta_value'  => '1',
		)
	);
	if ( empty( $flagged ) ) {
		return;
	}
	echo '<div class="notice notice-warning"><p><strong>Workshops:</strong> the event date could not be read on the following, so Past/Upcoming was not set. Please check the date field:</p><ul style="list-style:disc;margin-left:20px">';
	foreach ( $flagged as $p ) {
		printf(
			'<li><a href="%s">%s</a> — &ldquo;%s&rdquo;</li>',
			esc_url( get_edit_post_link( $p->ID ) ),
			esc_html( get_the_title( $p ) ),
			esc_html( (string) get_post_meta( $p->ID, 'display_date', true ) )
		);
	}
	echo '</ul></div>';
} );
