<?php
/**
 * Shard Cache - LRU Implementation
 *
 * Provides an LRU (Least Recently Used) cache for loaded index shards.
 * Limits memory usage by evicting oldest shards when capacity is exceeded.
 *
 * @package AIPT\Api\StoreIndex
 * @since 2.5.0
 */

namespace AIPT\Api\StoreIndex;

/**
 * ShardCache class
 *
 * Implements an LRU cache for shard data to minimize disk I/O
 * while keeping memory usage bounded.
 */
class ShardCache {

	/**
	 * Default maximum number of shards to cache.
	 */
	public const DEFAULT_MAX_SHARDS = 3;

	/**
	 * Cache storage.
	 * Structure: [cache_key => ['data' => array, 'access_time' => float]]
	 *
	 * @var array
	 */
	private $cache = array();

	/**
	 * Maximum number of shards to keep in cache.
	 *
	 * @var int
	 */
	private $max_size;

	/**
	 * Cache statistics for debugging.
	 *
	 * @var array
	 */
	private $stats = array(
		'hits'      => 0,
		'misses'    => 0,
		'evictions' => 0,
	);

	/**
	 * Constructor.
	 *
	 * @param int $max_size Maximum shards to cache. Defaults to filter value or DEFAULT_MAX_SHARDS.
	 */
	public function __construct( int $max_size = 0 ) {
		if ( $max_size <= 0 ) {
			/**
			 * Filter the maximum number of shards to cache.
			 *
			 * @since 2.5.0
			 * @param int $max_size Default cache size.
			 */
			$max_size = (int) apply_filters( 'aipt_shard_cache_max_size', self::DEFAULT_MAX_SHARDS );
		}

		$this->max_size = max( 1, $max_size );
	}

	/**
	 * Get a shard from cache.
	 *
	 * @param int $shard_id Shard ID.
	 * @return array|null Shard data or null if not cached.
	 */
	public function get( int $shard_id ): ?array {
		$cache_key = $this->getCacheKey( $shard_id );

		if ( isset( $this->cache[ $cache_key ] ) ) {

			$this->cache[ $cache_key ]['access_time'] = microtime( true );
			++$this->stats['hits'];
			return $this->cache[ $cache_key ]['data'];
		}

		++$this->stats['misses'];
		return null;
	}

	/**
	 * Store a shard in cache.
	 *
	 * Evicts the least recently used shard if cache is at capacity.
	 *
	 * @param int   $shard_id Shard ID.
	 * @param array $data     Shard data.
	 * @return void
	 */
	public function put( int $shard_id, array $data ): void {
		$cache_key = $this->getCacheKey( $shard_id );

		if ( isset( $this->cache[ $cache_key ] ) ) {
			$this->cache[ $cache_key ] = array(
				'data'        => $data,
				'access_time' => microtime( true ),
			);
			return;
		}

		if ( count( $this->cache ) >= $this->max_size ) {
			$this->evictLRU();
		}

		$this->cache[ $cache_key ] = array(
			'data'        => $data,
			'access_time' => microtime( true ),
		);
	}

	/**
	 * Check if a shard is in cache.
	 *
	 * @param int $shard_id Shard ID.
	 * @return bool True if cached.
	 */
	public function has( int $shard_id ): bool {
		return isset( $this->cache[ $this->getCacheKey( $shard_id ) ] );
	}

	/**
	 * Remove a specific shard from cache.
	 *
	 * @param int $shard_id Shard ID.
	 * @return bool True if removed, false if not in cache.
	 */
	public function remove( int $shard_id ): bool {
		$cache_key = $this->getCacheKey( $shard_id );

		if ( isset( $this->cache[ $cache_key ] ) ) {
			unset( $this->cache[ $cache_key ] );
			return true;
		}

		return false;
	}

	/**
	 * Clear all cached shards.
	 *
	 * @return void
	 */
	public function clear(): void {
		$this->cache = array();
	}

	/**
	 * Get the number of cached shards.
	 *
	 * @return int Cache size.
	 */
	public function size(): int {
		return count( $this->cache );
	}

	/**
	 * Get cache statistics.
	 *
	 * @return array Stats with keys: hits, misses, evictions, hit_rate, size, max_size.
	 */
	public function getStats(): array {
		$total    = $this->stats['hits'] + $this->stats['misses'];
		$hit_rate = $total > 0 ? ( $this->stats['hits'] / $total ) * 100 : 0;

		return array_merge(
			$this->stats,
			array(
				'hit_rate' => round( $hit_rate, 2 ),
				'size'     => count( $this->cache ),
				'max_size' => $this->max_size,
			)
		);
	}

	/**
	 * Reset cache statistics.
	 *
	 * @return void
	 */
	public function resetStats(): void {
		$this->stats = array(
			'hits'      => 0,
			'misses'    => 0,
			'evictions' => 0,
		);
	}

	/**
	 * Evict the least recently used shard from cache.
	 *
	 * @return void
	 */
	private function evictLRU(): void {
		if ( empty( $this->cache ) ) {
			return;
		}

		$lru_key  = null;
		$lru_time = PHP_FLOAT_MAX;

		foreach ( $this->cache as $key => $entry ) {
			if ( $entry['access_time'] < $lru_time ) {
				$lru_time = $entry['access_time'];
				$lru_key  = $key;
			}
		}

		if ( $lru_key !== null ) {
			unset( $this->cache[ $lru_key ] );
			++$this->stats['evictions'];
		}
	}

	/**
	 * Generate cache key for a shard.
	 *
	 * @param int $shard_id Shard ID.
	 * @return string Cache key.
	 */
	private function getCacheKey( int $shard_id ): string {
		return 'shard_' . $shard_id;
	}

	/**
	 * Get maximum cache size.
	 *
	 * @return int Maximum shards allowed in cache.
	 */
	public function getMaxSize(): int {
		return $this->max_size;
	}
}
