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

use QuantumCache\Store\MySQLStore;
use QuantumCache\Store\RedisStore;

/**
 * Lightweight cache stats for the active backend.
 * - MySQL: exact counts; cheap single queries.
 * - Redis: approximate counts via SCAN with a hard cap, safe for admin clicks.
 */
final class Stats {

	/**
	 * @return array{
	 *   backend: string,
	 *   entries: int,
	 *   tags: int,
	 *   bytes: int|null,
	 *   oldest: string|null,
	 *   newest: string|null,
	 *   approximate: bool,
	 *   note: string|null
	 * }
	 */
	public static function collect(): array {
		$store = StoreFactory::get();

		if ( $store instanceof MySQLStore ) {
			return self::mysql_stats();
		}

		if ( $store instanceof RedisStore ) {
			return self::redis_stats();
		}

		return array(
			'backend'     => 'unknown',
			'entries'     => 0,
			'tags'        => 0,
			'bytes'       => null,
			'oldest'      => null,
			'newest'      => null,
			'approximate' => true,
			'note'        => 'Unsupported store for stats.',
		);
	}

	private static function mysql_stats(): array {
		global $wpdb;

				$entries_raw = (string) $wpdb->prefix . 'qc_entries';
				$tagmap_raw  = (string) $wpdb->prefix . 'qc_tagmap';

		// Defensive: ensure table identifiers are safe (alnum + underscore)
		foreach ( array( $entries_raw, $tagmap_raw ) as $tbl ) {
			if ( 1 !== preg_match( '/^[A-Za-z0-9_]+$/', $tbl ) ) {
				return array(
					'backend'     => 'mysql',
					'entries'     => 0,
					'tags'        => 0,
					'bytes'       => 0,
					'oldest'      => null,
					'newest'      => null,
					'approximate' => true,
					'note'        => 'Invalid table name.',
				);
			}
		}
				// esc_sql() is used here to satisfy tooling; identifiers are still validated above.
				$entries = esc_sql( $entries_raw );
				$tagmap  = esc_sql( $tagmap_raw );

		/**
		 * The following direct queries:
		 * - Use only validated internal identifiers
		 * - Cannot use placeholders for table names
		 * - Are safe because no user input is interpolated
		 *
		 * @phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
		 * @phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
		 * @phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		 */

		// Count entries + total byte size + oldest/newest timestamps.
		// The qc_entries schema uses `updated` (not `created_at`).
		$row = $wpdb->get_row(
			"
        SELECT
            COUNT(*)          AS c,
            COALESCE(SUM(sz),0) AS bytes,
            MIN(updated)      AS oldest,
            MAX(updated)      AS newest
        FROM `{$entries}`
        ",
			ARRAY_A
		);

		// Count tags
		$tags = (int) $wpdb->get_var(
			"SELECT COUNT(*) FROM `{$tagmap}`"
		);

		/** @phpcs:enable */

		// If qc tables do not exist yet, get_row returns null.
		if ( ! is_array( $row ) ) {
			return array(
				'backend'     => 'mysql',
				'entries'     => 0,
				'tags'        => $tags,
				'bytes'       => 0,
				'oldest'      => null,
				'newest'      => null,
				'approximate' => true,
				'note'        => 'Tables not initialized yet.',
			);
		}

		return array(
			'backend'     => 'mysql',
			'entries'     => (int) $row['c'],
			'tags'        => (int) $tags,
			'bytes'       => (int) $row['bytes'],
			'oldest'      => $row['oldest'],
			'newest'      => $row['newest'],
			'approximate' => false,
			'note'        => null,
		);
	}



	private static function redis_stats(): array {
		// Make a fresh, short-timeout client; do not rely on internal instance.
		if ( ! class_exists( 'Redis' ) ) {
			return array(
				'backend'     => 'redis',
				'entries'     => 0,
				'tags'        => 0,
				'bytes'       => null,
				'oldest'      => null,
				'newest'      => null,
				'approximate' => true,
				'note'        => 'Redis class not available.',
			);
		}

		$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 );

		$r = new \Redis();
		try {
			if ( ! $r->connect( $host, $port, 0.5 ) ) {
				throw new \RuntimeException( 'connect fail' );
			}
		} catch ( \Throwable $e ) {
			// Fallback to docker service name if localhost.
			if ( in_array( $host, array( '127.0.0.1', 'localhost' ), true ) ) {
				try {
					if ( ! $r->connect( 'redis', $port, 0.5 ) ) {
						throw new \RuntimeException( 'connect fail (redis)' );
					}
				} catch ( \Throwable $e2 ) {
					return array(
						'backend'     => 'redis',
						'entries'     => 0,
						'tags'        => 0,
						'bytes'       => null,
						'oldest'      => null,
						'newest'      => null,
						'approximate' => true,
						'note'        => 'Redis not reachable.',
					);
				}
			} else {
				return array(
					'backend'     => 'redis',
					'entries'     => 0,
					'tags'        => 0,
					'bytes'       => null,
					'oldest'      => null,
					'newest'      => null,
					'approximate' => true,
					'note'        => 'Redis not reachable.',
				);
			}
		}

		if ( $db ) {
			$r->select( $db );
		}

		$prefix = StoreFactory::sitePrefix(); // e.g. "site:0:"
		$match  = $prefix . '*';

		// Cheap SCAN pass with hard limits to avoid blocking Redis.
		$it             = null;
		$max_scans      = 100;  // safety cap
		$scan_count_arg = 1000; // hint per SCAN
		$entries        = 0;
		$tags           = 0;

		for ( $i = 0; $i < $max_scans; $i++ ) {
			$keys = $r->scan( $it, $match, $scan_count_arg );
			if ( false === $keys ) {
						break;
			}
			foreach ( $keys as $k ) {
				// Keys for values look like "site:0:frag:..." or "site:0:query:..."
				// Tag sets are stored as "site:0:tag:...".
				if ( str_starts_with( $k, $prefix . 'tag:' ) ) {
					++$tags;
				} else {
					// ignore locks ("site:0:lock:...") from counts
					if ( str_starts_with( $k, $prefix . 'lock:' ) ) {
						continue;
					}
					++$entries;
				}
			}
			if ( 0 === $it ) {
				break;
			}
		}

		return array(
			'backend'     => 'redis',
			'entries'     => (int) $entries,
			'tags'        => (int) $tags,
			'bytes'       => null, // Measuring MEMORY USAGE per key is expensive; skip.
			'oldest'      => null,
			'newest'      => null,
			'approximate' => true,
			'note'        => 'Counts are approximate (bounded SCAN).',
		);
	}
}
