<?php
/**
 * In-memory click event repository.
 *
 * @package Affilun\Infrastructure\Persistence
 */

declare(strict_types=1);

namespace Affilun\Infrastructure\Persistence;

use DateTimeImmutable;
use DateTimeZone;

/**
 * Test double for click tracking persistence.
 */
final class InMemoryClickEventRepository implements ClickEventRepository {

	/**
	 * @var list<array<string,mixed>>
	 */
	private array $events = array();

	/**
	 * @var array<string,int>
	 */
	private array $rollups = array();

	/**
	 * Record a sanitized click event.
	 *
	 * @param array<string,mixed> $event Sanitized event payload.
	 */
	public function record( array $event ): void {
		$this->events[] = $event;
		foreach ( self::rollupKeys( $event ) as $key ) {
			$this->rollups[ $key ] = ( $this->rollups[ $key ] ?? 0 ) + 1;
		}
	}

	/**
	 * Return recent click events.
	 *
	 * @param int $limit Maximum rows.
	 *
	 * @return list<array<string,mixed>>
	 */
	public function recentEvents( int $limit = 500 ): array {
		if ( $limit <= 0 ) {
			return array();
		}
		return array_slice( array_reverse( $this->events ), 0, $limit );
	}

	/**
	 * Count recent events matching a client bucket.
	 *
	 * @param string            $userAgentHash User-agent hash.
	 * @param string            $refererHost   Referer host.
	 * @param DateTimeImmutable $since         Lower bound.
	 */
	public function countRecentMatching( string $userAgentHash, string $refererHost, DateTimeImmutable $since ): int {
		$since = $since->setTimezone( new DateTimeZone( 'UTC' ) );
		$count = 0;
		foreach ( $this->events as $event ) {
			$createdAt = self::createdAt( $event );
			$metadata  = isset( $event['metadata'] ) && is_array( $event['metadata'] ) ? $event['metadata'] : array();
			if ( $createdAt === null || $createdAt < $since ) {
				continue;
			}
			if ( self::stringField( $metadata, 'user_agent_hash' ) === $userAgentHash && self::stringField( $metadata, 'referer_host' ) === $refererHost ) {
				++$count;
			}
		}
		return $count;
	}

	/**
	 * Delete click events older than cutoff.
	 *
	 * @param DateTimeImmutable $cutoff Cutoff instant.
	 */
	public function deleteOlderThan( DateTimeImmutable $cutoff ): int {
		$cutoff  = $cutoff->setTimezone( new DateTimeZone( 'UTC' ) );
		$kept    = array();
		$deleted = 0;
		foreach ( $this->events as $event ) {
			$createdAt = self::createdAt( $event );
			if ( $createdAt !== null && $createdAt < $cutoff ) {
				++$deleted;
				continue;
			}
			$kept[] = $event;
		}
		$this->events  = $kept;
		$this->rollups = array();
		foreach ( $this->events as $event ) {
			foreach ( self::rollupKeys( $event ) as $key ) {
				$this->rollups[ $key ] = ( $this->rollups[ $key ] ?? 0 ) + 1;
			}
		}
		return $deleted;
	}

	/**
	 * @return list<array<string,mixed>>
	 */
	public function events(): array {
		return $this->events;
	}

	/**
	 * @return array<string,int>
	 */
	public function rollups(): array {
		return $this->rollups;
	}

	/**
	 * Read a string field.
	 *
	 * @param array<string,mixed> $event Event.
	 * @param string              $field Field name.
	 */
	private static function stringField( array $event, string $field ): string {
		$value = $event[ $field ] ?? null;
		return is_string( $value ) ? $value : '';
	}

	/**
	 * Parse event creation timestamp.
	 *
	 * @param array<string,mixed> $event Event.
	 */
	private static function createdAt( array $event ): ?DateTimeImmutable {
		$value = $event['created_at'] ?? null;
		if ( ! is_string( $value ) || trim( $value ) === '' ) {
			return null;
		}
		try {
			return ( new DateTimeImmutable( $value ) )->setTimezone( new DateTimeZone( 'UTC' ) );
		} catch ( \Throwable $unused ) {
			unset( $unused );
			return null;
		}
	}

	/**
	 * @param array<string,mixed> $event Event.
	 *
	 * @return list<string>
	 */
	private static function rollupKeys( array $event ): array {
		$keys = array();
		foreach ( array( 'provider', 'button_key', 'product_id', 'post_id', 'variant' ) as $field ) {
			if ( isset( $event[ $field ] ) && ( is_string( $event[ $field ] ) || is_int( $event[ $field ] ) ) ) {
				$keys[] = $field . ':' . (string) $event[ $field ];
			}
		}
		return $keys;
	}
}
