<?php
/**
 * Abstract Transient Base
 *
 * Provides a structured way to manage transients with support for grouping and versioning.
 * This structure allows for efficient cache invalidation strategies, enabling both broad
 * and targeted cache clearing as needed.
 *
 * @package AmbassadorHub\Cache
 */

namespace AmbassadorHub\Cache;

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

abstract class AbstractTransientBase {

	protected const OPTION_TRANSIENT_INDEX_VERSION = 'ambh_index_version';

	/**
	 * Empty by default; GroupTransient enforces override.
	 */
	protected const TRANSIENT_GROUP = '';

	/**
	 * Get a safe transient key by applying sanitization and hashing to ensure it fits within
	 * length limits and is unique.
	 *
	 * Combines a sanitized version of the raw key with a hash to ensure uniqueness,
	 * even if the sanitized version is not unique.
	 *
	 * @param string $raw_key The original key to be transformed into a transient key.
	 * @return string A sanitized and hashed transient key suitable for use with WordPress transients.
	 */
	protected static function get_safe_key( string $raw_key ): string {
		$readable = sanitize_key( $raw_key );
		$short    = substr( $readable, 0, 70 );
		$hash     = substr( md5( $raw_key ), 0, 14 );

		$base_key = "{$short}_{$hash}";
		return 'ambh_' . sanitize_key( $base_key );
	}

	/**
	 * Get the group name for this transient. Used for versioning and invalidation.
	 *
	 * By default, returns empty string for global transients.
	 * Grouped transients must override to return a non-empty group name.
	 *
	 * @return string Group name, or empty string for global transients.
	 */
	protected static function get_group(): string {
		return sanitize_key( static::TRANSIENT_GROUP );
	}

	/**
	 * Get the options key used to store the version for this transient's group.
	 *
	 * @return string Options key for the transient group version.
	 */
	protected static function get_group_option(): string {
		$group = static::get_group();
		return '' === $group ? self::OPTION_TRANSIENT_INDEX_VERSION : self::OPTION_TRANSIENT_INDEX_VERSION . '_' . $group;
	}

	/**
	 * Build a fully-versioned transient key from a base key.
	 *
	 * Incorporates both the global version and — for grouped transients — the group version,
	 * so bumping either version effectively invalidates matching transients.
	 *
	 * @param string $base_key The base key for the transient (without group or version).
	 * @return string Fully-versioned transient key.
	 */
	private static function get_versioned_key( string $base_key ): string {
		$global_version = (int) get_option( self::OPTION_TRANSIENT_INDEX_VERSION, 0 );

		$group    = static::get_group();
		$safe_key = self::get_safe_key( $base_key );

		if ( '' === $group ) {
			// Global transient: no group version.
			return "{$safe_key}-v{$global_version}";
		}

		$group_option  = self::get_group_option();
		$group_version = (int) get_option( $group_option, 0 );

		return "{$safe_key}-v{$global_version}-{$group}-v{$group_version}";
	}

	/**
	 * Get a transient value by base key, applying group and global versioning.
	 *
	 * @param string $base_key The base key for the transient (without group or version).
	 * @return mixed The transient value, or false if not found.
	 */
	public static function get( string $base_key ): mixed {
		$key   = self::get_versioned_key( $base_key );
		$value = get_transient( $key );
		return ( false === $value ) ? false : $value;
	}

	/**
	 * Set a transient value by base key, applying group and global versioning.
	 *
	 * @param string $base_key   The base key for the transient (without group or version).
	 * @param mixed  $value      The value to store in the transient.
	 * @param int    $expiration Expiration time in seconds.
	 */
	public static function set( string $base_key, mixed $value, int $expiration ): void {
		$key = self::get_versioned_key( $base_key );
		set_transient( $key, $value, $expiration );
	}

	/**
	 * Delete a transient by base key, applying group and global versioning.
	 *
	 * @param string $base_key The base key for the transient (without group or version).
	 */
	public static function delete( string $base_key ): void {
		$key = self::get_versioned_key( $base_key );
		delete_transient( $key );
	}

	/**
	 * Bump the global transient version, invalidating all versioned transients.
	 *
	 * Protected on purpose: only GlobalTransient exposes this publicly.
	 */
	protected static function bump_global_version(): void {
		$version = (int) get_option( self::OPTION_TRANSIENT_INDEX_VERSION, 0 );
		update_option( self::OPTION_TRANSIENT_INDEX_VERSION, $version + 1 );
	}
}
