<?php
/**
 * Safe event cache persistence.
 *
 * @package VoidPulse
 */

namespace VoidPulse\Infrastructure;

defined( 'ABSPATH' ) || exit;

final class EventRepository {
	/**
	 * Upsert a sanitized event.
	 *
	 * @param array<string,mixed> $event Event.
	 * @return bool
	 */
	public function upsert( $event ) {
		global $wpdb;
		$table = $wpdb->prefix . 'voidpulse_events';
		$sql   = $wpdb->prepare(
			"INSERT INTO %i (source,source_record_id,event_type,product_id,variation_id,event_timestamp,payload,privacy_level,expires_at,created_at)
			VALUES (%s,%s,%s,%d,%d,%s,%s,%s,%s,%s)
			ON DUPLICATE KEY UPDATE event_timestamp=VALUES(event_timestamp),payload=VALUES(payload),expires_at=VALUES(expires_at)",
			$table,
			'woocommerce_order',
			(string) $event['source_record_id'],
			'recent_sale',
			absint( $event['product_id'] ),
			absint( $event['variation_id'] ),
			(string) $event['event_timestamp'],
			wp_json_encode( $event['payload'] ),
			'safe-v1',
			(string) $event['expires_at'],
			gmdate( 'Y-m-d H:i:s' )
		);
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Plugin-owned event upsert; related caches are invalidated below.
		$result = false !== $wpdb->query( $sql );
		wp_cache_delete( 'events_newest', 'voidpulse' );
		wp_cache_delete( 'events_random', 'voidpulse' );
		return $result;
	}

	/**
	 * Clear the derived WooCommerce sale cache before an authoritative refresh.
	 *
	 * @return bool
	 */
	public function clear_sales() {
		global $wpdb;
		$table = $wpdb->prefix . 'voidpulse_events';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Derived plugin-owned cache is rebuilt immediately by the collector.
		$result = $wpdb->delete( $table, array( 'source' => 'woocommerce_order' ) );
		wp_cache_delete( 'events_newest', 'voidpulse' );
		wp_cache_delete( 'events_random', 'voidpulse' );
		return false !== $result;
	}

	/**
	 * Delete the cached events identified by their record keys.
	 *
	 * @param array<int,string> $keys Record keys.
	 * @return int Number of rows removed.
	 */
	public function delete_by_keys( $keys ) {
		global $wpdb;
		$keys = array_slice( array_values( array_unique( array_filter( array_map( 'strval', (array) $keys ) ) ) ), 0, 200 );
		if ( ! $keys ) {
			return 0;
		}
		$table        = $wpdb->prefix . 'voidpulse_events';
		$placeholders = implode( ',', array_fill( 0, count( $keys ), '%s' ) );
		$sql          = "DELETE FROM %i WHERE source_record_id IN ({$placeholders})";
		array_unshift( $keys, $table );
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic placeholders match the bounded key list; the clause itself is fixed.
		$sql = $wpdb->prepare( $sql, $keys );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Bounded plugin-owned delete; related caches are invalidated below.
		$removed = (int) $wpdb->query( $sql );
		wp_cache_delete( 'events_newest', 'voidpulse' );
		wp_cache_delete( 'events_random', 'voidpulse' );
		return $removed;
	}

	/**
	 * Get display-safe recent events.
	 *
	 * @param int    $limit Limit.
	 * @param string $order Order.
	 * @return array<int,array<string,mixed>>
	 */
	public function recent( $limit, $order = 'newest' ) {
		global $wpdb;
		$cache_key = 'events_' . ( 'random' === $order ? 'random' : 'newest' );
		$cached    = wp_cache_get( $cache_key, 'voidpulse' );
		if ( is_array( $cached ) && count( $cached ) >= min( absint( $limit ), 10 ) ) {
			return array_slice( $cached, 0, absint( $limit ) );
		}
		$table     = $wpdb->prefix . 'voidpulse_events';
		$direction = 'random' === $order ? 'RAND()' : 'event_timestamp DESC';
		$limit     = min( 200, max( 1, absint( $limit ) ) );
		if ( 'random' === $order ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Plugin-owned event table; results are cached below.
			$rows = $wpdb->get_results( $wpdb->prepare( "SELECT source_record_id,payload,event_timestamp FROM %i WHERE event_type='recent_sale' AND expires_at > UTC_TIMESTAMP() ORDER BY RAND() LIMIT %d", $table, $limit ), ARRAY_A );
		} else {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Plugin-owned event table; results are cached below.
			$rows = $wpdb->get_results( $wpdb->prepare( "SELECT source_record_id,payload,event_timestamp FROM %i WHERE event_type='recent_sale' AND expires_at > UTC_TIMESTAMP() ORDER BY event_timestamp DESC LIMIT %d", $table, $limit ), ARRAY_A );
		}
		$out  = array();
		foreach ( $rows as $row ) {
			$payload = json_decode( $row['payload'], true );
			if ( is_array( $payload ) ) {
				$payload['event_key'] = sanitize_text_field( $row['source_record_id'] );
				$out[] = $payload;
			}
		}
		wp_cache_set( $cache_key, $out, 'voidpulse', 30 );
		return $out;
	}

	/**
	 * Delete expired/cache-overflow records in bounded batches.
	 *
	 * @param int $maximum Maximum cache size.
	 * @return void
	 */
	public function cleanup( $maximum = 100 ) {
		global $wpdb;
		$table   = $wpdb->prefix . 'voidpulse_events';
		$maximum = min( 500, max( 10, absint( $maximum ) ) );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bounded scheduled cleanup of expired rows in a plugin-owned table.
		$wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE expires_at <= UTC_TIMESTAMP() LIMIT 500', $table ) );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bounded scheduled retention cleanup in a plugin-owned table.
		$wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE id NOT IN (SELECT id FROM (SELECT id FROM %i ORDER BY event_timestamp DESC LIMIT %d) retained)', $table, $table, $maximum ) );
		wp_cache_delete( 'events_newest', 'voidpulse' );
		wp_cache_delete( 'events_random', 'voidpulse' );
	}
}
