<?php declare( strict_types=1 );

namespace KadenceWP\KadenceStarterTemplates\Image_Downloader;

use KadenceWP\KadenceStarterTemplates\Hasher;
use KadenceWP\KadenceStarterTemplates\Psr\Log\LoggerInterface;
use KadenceWP\KadenceStarterTemplates\Shutdown\Contracts\Terminable;
use KadenceWP\KadenceStarterTemplates\Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;

/**
 * Prime Pexels HTTP cache for future image downloading.
 */
final class Cache_Primer implements Terminable {

	/**
	 * @var HttpClientInterface
	 */
	private $client;

	/**
	 * @var LoggerInterface
	 */
	private $logger;

	/**
	 * @var Hasher
	 */
	private $hasher;

	/**
	 * How long in seconds to wait until we remotely prime the collection of images again.
	 *
	 * @var int Time in seconds.
	 */
	private $cache_duration;

	/**
	 * How many external cache requests to create before removing them.
	 *
	 * @var int
	 */
	private $batch_size;

	/**
	 * The collections cache, to run when the class is destroyed.
	 *
	 * @var array<array{
	 *         collection_slug: string,
	 *         image_type: string,
	 *         images: array<int, array{
	 *              id: int,
	 *              width: int,
	 *              height: int,
	 *              alt: string,
	 *              url: string,
	 *              photographer: string,
	 *              photographer_url: string,
	 *              avg_color: string,
	 *              sizes: non-empty-array<int,array{name: string, src: string}>
	 *         }>
	 *        }> $collections
	 */
	private $collections;

	/**
	 * @param HttpClientInterface $client         The HTTP client.
	 * @param LoggerInterface     $logger         The logger.
	 * @param Hasher              $hasher         The hasher.
	 * @param int                 $cache_duration The cache duration in seconds.
	 * @param int                 $batch_size     How many external cache requests to create before removing them.
	 */
	public function __construct(
		HttpClientInterface $client,
		LoggerInterface $logger,
		Hasher $hasher,
		int $cache_duration,
		int $batch_size = 500
	) {
		$this->client         = $client;
		$this->logger         = $logger;
		$this->hasher         = $hasher;
		$this->cache_duration = $cache_duration;
		$this->batch_size     = $batch_size;
	}

	/**
	 * Assign which collections will be primed on the WordPress shutdown hook.
	 *
	 * @param array<array{
	 *        collection_slug: string,
	 *        image_type: string,
	 *        images: array<int, array{
	 *             id: int,
	 *             width: int,
	 *             height: int,
	 *             alt: string,
	 *             url: string,
	 *             photographer: string,
	 *             photographer_url: string,
	 *             avg_color: string,
	 *             sizes: non-empty-array<int,array{name: string, src: string}>
	 *        }>
	 *       }> $collections
	 *
	 * @return void
	 */
	public function init( array $collections ): void {
		if ( empty( $collections ) ) {
			return;
		}

		$this->collections = $collections;
	}

	/**
	 * Prime cache on shutdown.
	 *
	 * @action shutdown
	 *
	 * @return void
	 * @throws \InvalidArgumentException
	 * @throws \RuntimeException
	 */
	public function terminate(): void {
		$this->execute();
	}

	/**
	 * On shutdown, make asynchronous HEAD requests to all the potential images we'll download
	 * to ensure that Pexels caches their response to make downloading much quicker.
	 *
	 * This allows for 0 blocking.
	 *
	 * @action shutdown
	 *
	 * @return void
	 *
	 * @throws \InvalidArgumentException
	 * @throws \RuntimeException
	 */
	private function execute(): void {
		if ( ! isset( $this->collections ) ) {
			return;
		}

		$batch     = 0;
		$cache_key = $this->hasher->hash( $this->collections );

		if ( get_transient( $cache_key ) !== false ) {
			$this->logger->debug( sprintf( 'Found cache key "%s", skipping image cache priming', $cache_key ) );

			unset( $this->collections );

			return;
		}

		// Search results differ slightly from industry collections, reformat.
		if ( isset( $this->collections['images'] ) ) {
			$this->collections = [ $this->collections ];
		}

		foreach ( $this->collections as $collection ) {

			$this->logger->debug( sprintf( 'Priming image cache for %d images...', count( $collection['images'] ) ) );

			foreach ( $collection['images'] as $image ) {

				$this->logger->debug( sprintf( 'Priming image cache for: %s', $image['url'] ) );

				foreach ( $image['sizes'] as $size ) {
					try {
						$batch++;

						$url = $size['src'];

						// These are async requests; We won't wait for the responses.
						$promises[ $url ] = $this->client->request( 'HEAD', $url, [
							'timeout'      => 0.1,
							'max_duration' => 0.1,
						] );
					} catch ( Throwable $e ) {
					}

					// Remove existing promises when batch size reached.
					if ( $batch >= $this->batch_size ) {
						$batch = 0;

						try {
							unset( $promises );
						} catch ( Throwable $e ) {
						}
					}
				}
			}
		}

		// Remove any remaining promises.
		try {
			unset( $promises );
		} catch ( Throwable $e ) {
		}

		// Clear collections state in case this is accessed again in the same request.
		unset( $this->collections );

		$this->logger->debug( sprintf( 'Caching image priming using key "%s" for %d seconds', $cache_key, $this->cache_duration ) );

		set_transient( $cache_key, true, $this->cache_duration );
	}

}
