<?php

namespace MediaWiki\Extension\AbuseFilter;

use Wikimedia\ObjectCache\BagOStuff;

/**
 * Helper class for EmergencyWatcher. Wrapper around cache which tracks hits of recently
 * modified filters.
 */
class EmergencyCache {

	public const SERVICE_NAME = ServiceNames::EmergencyCache;

	public function __construct(
		private readonly BagOStuff $stash,
		private readonly array $ttlPerGroup
	) {
	}

	/**
	 * Get recently modified filters in the group. Thanks to this, performance can be improved,
	 * because only a small subset of filters will need an update.
	 *
	 * @param string $group
	 * @return int[]
	 */
	public function getFiltersToCheckInGroup( string $group ): array {
		$filterToExpiry = $this->stash->get( $this->createGroupKey( $group ) );
		if ( $filterToExpiry === false ) {
			return [];
		}
		$time = (int)round( $this->stash->getCurrentTime() );
		return array_keys( array_filter(
			$filterToExpiry,
			static fn ( $exp ) => $exp > $time
		) );
	}

	/**
	 * Create a new entry in cache for a filter and update the entry for the group.
	 * This method is usually called after the filter has been updated.
	 *
	 * @param int $filter
	 * @param string $group
	 * @return bool
	 */
	public function setNewForFilter( int $filter, string $group ): bool {
		$ttl = $this->ttlPerGroup[$group] ?? $this->ttlPerGroup['default'];
		$expiry = (int)round( $this->stash->getCurrentTime() + $ttl );
		$this->stash->merge(
			$this->createGroupKey( $group ),
			static function ( $cache, $key, $value ) use ( $filter, $expiry ) {
				if ( $value === false ) {
					$value = [];
				}
				// note that some filters may have already had their keys expired
				// we are currently filtering them out in getFiltersToCheckInGroup
				// but if necessary, it can be done here
				$value[$filter] = $expiry;
				return $value;
			},
			$expiry
		);
		return $this->stash->set(
			$this->createFilterKey( $filter ),
			[ 'total' => 0, 'matches' => 0, 'expiry' => $expiry ],
			$expiry
		);
	}

	/**
	 * Increase the filter's 'total' value by one and possibly also the 'matched' value.
	 *
	 * @param int $filter
	 * @param bool $matched Whether the filter matched the action
	 * @return bool
	 */
	public function incrementForFilter( int $filter, bool $matched ): bool {
		return $this->stash->merge(
			$this->createFilterKey( $filter ),
			static function ( $cache, $key, $value, &$expiry ) use ( $matched ) {
				if ( $value === false ) {
					return false;
				}
				$value['total']++;
				if ( $matched ) {
					$value['matches']++;
				}
				// enforce the prior TTL
				$expiry = $value['expiry'];
				return $value;
			}
		);
	}

	/**
	 * Get the cache entry for the filter. Returns false when the key has already expired.
	 * Otherwise it returns the entry formatted as [ 'total' => number of actions,
	 * 'matches' => number of hits ] (since the last filter modification).
	 *
	 * @param int $filter
	 * @return array|false
	 */
	public function getForFilter( int $filter ) {
		$value = $this->stash->get( $this->createFilterKey( $filter ) );
		if ( $value !== false ) {
			unset( $value['expiry'] );
		}
		return $value;
	}

	private function createGroupKey( string $group ): string {
		return $this->stash->makeKey( 'abusefilter-emergency', 'group', $group );
	}

	private function createFilterKey( int $filter ): string {
		return $this->stash->makeKey( 'abusefilter-emergency', 'filter', $filter );
	}

}
