<?php
namespace QuantumCache\Store;
\defined( 'ABSPATH' ) || exit;
use QuantumCache\Support\StoreFactory;

class RedisStore implements StoreInterface {

	/** @var \Redis|null */
	private $r;
	private string $site_prefix;

	public function __construct() {
		$this->site_prefix = StoreFactory::sitePrefix();
		if ( ! class_exists( 'Redis' ) ) {
			$this->r = null;
			return;
		}

		$host = defined( 'QUANTUMCACHE_REDIS_HOST' ) ? QUANTUMCACHE_REDIS_HOST : '127.0.0.1';
		$port = (int) ( defined( 'QUANTUMCACHE_REDIS_PORT' ) ? QUANTUMCACHE_REDIS_PORT : 6379 );
		$db   = (int) ( defined( 'QUANTUMCACHE_REDIS_DB' ) ? QUANTUMCACHE_REDIS_DB : 0 );

		$this->r = new \Redis();
		try {
			// First try configured host.
			if ( ! $this->r->connect( $host, $port, 1.0 ) ) {
				throw new \RuntimeException( 'connect fail' );
			}
		} catch ( \Throwable $e ) {
			// Fallback: try service name if host was localhost.
			try {
				if ( in_array( $host, array( '127.0.0.1', 'localhost' ), true ) ) {
					if ( ! $this->r->connect( 'redis', $port, 1.0 ) ) {
						throw new \RuntimeException( 'connect fail (redis)' );
					}
				} else {
					throw $e;
				}
			} catch ( \Throwable $e2 ) {
				// Disable; StoreFactory chose Redis, but we can't reach it.
				$this->r = null;
				return;
			}
		}
		if ( $db ) {
			$this->r->select( $db );
		}
	}

	private function k( string $suffix ): string {
		return $this->site_prefix . $suffix;
	}

	public function get( string $key ): ?string {
		if ( ! $this->r ) {
			return null;
		}
		$val = $this->r->get( $key );
		return ( false === $val ) ? null : $val; // Yoda
	}

	public function set( string $key, string $val, int $ttl, ?int $stale_ttl = null ): void {
		if ( $this->r ) {
			$this->r->setex( $key, max( 1, (int) $ttl ), $val );
		}
	}

	public function addTags( string $key, array $tags ): void {
		if ( ! $this->r ) {
			return;
		}
		$tags = array_values( array_unique( array_filter( array_map( 'strval', $tags ) ) ) );
		foreach ( $tags as $t ) {
			$this->r->sAdd( $this->k( 'tag:' . $t ), $key );
		}
	}

	public function purgeTags( array $tags ): int {
		// Schedule a batched CDN purge at shutdown so that cascading invalidations
		// (e.g. WooCommerce save_post chains) are coalesced into a single purge call
		// rather than making N sequential HTTP requests from within the same process.
		if ( ! empty( $tags ) && function_exists( 'qc_schedule_cdn_purge' ) ) {
			qc_schedule_cdn_purge( $tags );
		}
		if ( ! $this->r ) {
			return 0;
		}
		$count = 0;
		foreach ( $tags as $t ) {
			$set_key = $this->k( 'tag:' . $t );
			$members = $this->r->sMembers( $set_key );
			if ( false === $members || empty( $members ) ) {
				$members = array();
			}
			if ( ! empty( $members ) ) {
				$count += (int) $this->r->del( $members );
			}
			$this->r->del( $set_key );
		}
		return $count;
	}

	public function delete( string $key ): void {
		if ( $this->r ) {
			$this->r->del( $key );
		}
	}

	public function acquireLock( string $key, int $ms_ttl ): bool {
		if ( ! $this->r ) {
			return false;
		}
		return (bool) $this->r->set(
			$this->k( 'lock:' . $key ),
			(string) getmypid(),
			array(
				'nx',
				'px' => max( 1, $ms_ttl ),
			)
		);
	}

	public function releaseLock( string $key ): void {
		if ( $this->r ) {
			$this->r->del( $this->k( 'lock:' . $key ) );
		}
	}

	public function purgeAll(): int {
		$prefix  = \QuantumCache\Support\StoreFactory::sitePrefix();
		$pattern = $prefix . '*';

		$deleted = 0;
		$it      = null; // phpredis advances this by reference
		try {
			// Use a modest COUNT to avoid blocking; adjust if you want faster scans.
			$count_hint = 500;

			while ( true ) {
				// phpredis signature: scan(&$it, $pattern = null, $count = 0)
				$keys = $this->r->scan( $it, $pattern, $count_hint );

				if ( false === $keys ) {
					// No keys matched this round; continue until $it becomes 0.
					if ( 0 === $it ) {
						break;
					}
				} elseif ( ! empty( $keys ) ) {
					// DEL supports an array argument in recent phpredis versions.
					// Returns number of keys actually deleted.
					$deleted += (int) $this->r->del( $keys );
				}

				if ( 0 === $it ) {
					break;
				}
			}
		} catch ( \Throwable $e ) {
			// Don’t fatal in admin; just return what we managed to delete.
			return (int) $deleted;
		}

		return (int) $deleted;
	}
}
