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

class MySQLStore implements StoreInterface {

		/**
		 * This class implements QuantumCache's database-backed store.
		 * Direct, prepared $wpdb queries against our own tables are required.
		 * Caching is handled at the QC layer and via in-request memoization (self::$memo).
		 * Using wp_cache_* here would create double-caching and potential staleness issues.
		 *
         * @phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
         * @phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
		 */
	private string $table_entries;
	private string $table_tagmap;
	private string $site_prefix;

	public function __construct( \wpdb $wpdb ) {
						// Site-wide key prefix (blog + salt); keep in sync with RedisStore.
						$this->site_prefix = StoreFactory::sitePrefix();

			// Use the current blog’s prefix, not a dynamic property.
			$prefix = $wpdb->get_blog_prefix( 0 ); // multisite-safe

			$this->table_entries = $prefix . 'qc_entries';
			$this->table_tagmap  = $prefix . 'qc_tagmap';

			// Hard-validate identifiers: letters, numbers, underscore only.
		foreach ( array( $this->table_entries, $this->table_tagmap ) as $t ) {
			if ( ! preg_match( '/^[A-Za-z0-9_]+$/', $t ) ) {
				// Should never happen; defensive.
				throw new \RuntimeException( 'Invalid table identifier' );
			}
		}
	}


		/** Wrap a validated identifier in backticks. */
	private function ident( string $t ): string {
		// Given the regex check above, this is safe. No user input involved.
		return '`' . $t . '`';
	}


	public static function activate(): void {
		global $wpdb;
		require_once ABSPATH . 'wp-admin/includes/upgrade.php';

		$charset = $wpdb->get_charset_collate();
		$entries = $wpdb->prefix . 'qc_entries';
		$tagmap  = $wpdb->prefix . 'qc_tagmap';

		$sql1 = "CREATE TABLE {$entries} (
  khash   BINARY(16)    NOT NULL,
  v       LONGBLOB      NOT NULL,
  exp     INT UNSIGNED  NOT NULL DEFAULT 0,
  sz      INT UNSIGNED  NOT NULL,
  updated TIMESTAMP     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (khash),
  KEY exp_idx (exp)
) ENGINE=InnoDB {$charset};";

		$sql2 = "CREATE TABLE {$tagmap} (
  taghash BINARY(16) NOT NULL,
  khash   BINARY(16) NOT NULL,
  PRIMARY KEY (taghash, khash),
  KEY k_idx (khash)
) ENGINE=InnoDB {$charset};";

		dbDelta( $sql1 );
		dbDelta( $sql2 );
	}

	private function khash( string $key ): string {
		return pack( 'H*', md5( $this->site_prefix . $key ) );
	}

	private function thash( string $tag ): string {
		return pack( 'H*', md5( $this->site_prefix . $tag ) );
	}

	// In-request memo to avoid duplicate DB hits.
	private static array $memo = array();

	public function get( string $key ): ?string {
		if ( array_key_exists( $key, self::$memo ) ) {
			return self::$memo[ $key ];
		}

		global $wpdb;
		$k   = $this->khash( $key );
		$now = time();

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Validated table name; values use placeholders.
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT v FROM `{$this->table_entries}`
                 WHERE khash=%s AND (exp=0 OR exp>%d)
                 LIMIT 1",
				$k,
				$now
			),
			ARRAY_A
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		$val                = $row ? (string) $row['v'] : null;
		self::$memo[ $key ] = $val;
		return $val;
	}

	public function set( string $key, string $val, int $ttl, ?int $stale_ttl = null ): void {
		// $stale_ttl kept for API parity.
		global $wpdb;
		$k   = $this->khash( $key );
		$exp = ( 0 < $ttl ) ? ( time() + max( 1, (int) $ttl ) ) : 0;
		$sz  = strlen( $val );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Validated table name; values use placeholders.
		$wpdb->query(
			$wpdb->prepare(
				"INSERT INTO `{$this->table_entries}` (khash,v,exp,sz)
                 VALUES (%s,%s,%d,%d)
                 ON DUPLICATE KEY UPDATE v=VALUES(v), exp=VALUES(exp), sz=VALUES(sz)",
				$k,
				$val,
				$exp,
				$sz
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		self::$memo[ $key ] = $val;
	}

		/** Ensure $wpdb has typed properties for our tables (satisfies WPCS). */
	private function ensure_wpdb_tables(): void {
		global $wpdb;

		// Only set if not already defined.
		if ( empty( $wpdb->quantumcache_entries ) ) {
			$wpdb->quantumcache_entries = $wpdb->prefix . 'qc_entries';
		}
		if ( empty( $wpdb->quantumcache_tagmap ) ) {
			$wpdb->quantumcache_tagmap = $wpdb->prefix . 'qc_tagmap';
		}
	}


	public function addTags( string $key, array $tags ): void {
		global $wpdb;

		if ( empty( $tags ) ) {
			return;
		}

		$khash = $this->khash( $key );

		// Normalize and de-dup tags.
		$tags = array_values( array_unique( array_map( 'strval', $tags ) ) );

		// Validate the table identifier once; wpdb::insert() still wants the raw name.
		$table = (string) $this->table_tagmap;
		if ( 1 !== preg_match( '/^[A-Za-z0-9_]+$/', $table ) ) {
			return;
		}

		foreach ( $tags as $t ) {
			$thash = $this->thash( $t );

			// Use wpdb::insert with explicit formats. Duplicate rows will just fail and be skipped.
			$ok = $wpdb->insert(
				$table,
				array(
					'taghash' => $thash,
					'khash'   => $khash,
				),
				array( '%s', '%s' )
			);

			// Ignore duplicates or transient errors; no throw, no echo.
			if ( false === $ok ) {
				// Optionally: inspect $wpdb->last_error for non-dup errors.
				continue;
			}
		}
	}


	public function purgeTags( array $tags ): int {
		global $wpdb;

		$this->ensure_wpdb_tables();

		if ( empty( $tags ) ) {
			return 0;
		}

		// Schedule a batched CDN purge at shutdown (see qc_schedule_cdn_purge).
		if ( function_exists( 'qc_schedule_cdn_purge' ) ) {
			qc_schedule_cdn_purge( $tags );
		}

		// Hash & dedupe incoming tags.
		$ths = array_values(
			array_unique(
				array_map(
					function ( $t ) {
						return $this->thash( (string) $t );
					},
					$tags
				)
			)
		);

		if ( empty( $ths ) ) {
			return 0;
		}

		// Validated internal identifier: get_blog_prefix() . 'qc_tagmap', checked in __construct().
		$table = $this->table_tagmap;

		$khashes = array();

		foreach ( $ths as $th ) {
			$rows = $wpdb->get_col(
				$wpdb->prepare(
				// $table is a validated internal table name, not user input.
				// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
					'SELECT DISTINCT khash FROM `' . $table . '` WHERE taghash = %s',
					$th
				)
			);

			if ( ! empty( $rows ) ) {
				foreach ( $rows as $kh ) {
					$khashes[ (string) $kh ] = true;
				}
			}
		}

		if ( empty( $khashes ) ) {
			return 0;
		}

		// Delete rows by khash using wpdb helpers.
		$total = 0;

		foreach ( array_keys( $khashes ) as $kh ) {
			$del1 = $wpdb->delete(
				$wpdb->quantumcache_entries,
				array( 'khash' => (string) $kh ),
				array( '%s' )
			);

			if ( false !== $del1 ) {
				$total += (int) $del1;
			}

			$wpdb->delete(
				$wpdb->quantumcache_tagmap,
				array( 'khash' => (string) $kh ),
				array( '%s' )
			);
		}

		return $total;
	}





	public function purgeAll(): int {
		global $wpdb;

		/**
		 * Bulk-delete all QuantumCache-owned rows for this site.
		 * Table names are validated identifiers (alphanumeric + underscore only;
		 * confirmed in __construct). TRUNCATE is not used because it resets
		 * AUTO_INCREMENT and is DDL — DELETE keeps the tables intact and returns
		 * an affected-row count via $wpdb->rows_affected.
		 *
		 * @phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
		 * @phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
		 * @phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		 */
		$wpdb->query( "DELETE FROM `{$this->table_tagmap}`" );
		$wpdb->query( "DELETE FROM `{$this->table_entries}`" );
		/** @phpcs:enable */

		// rows_affected is set by the last query; return entries count.
		return (int) $wpdb->rows_affected;
	}

	public function delete( string $key ): void {
		global $wpdb;
		$k = $this->khash( $key );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$wpdb->query( $wpdb->prepare( "DELETE FROM `{$this->table_entries}` WHERE khash=%s", $k ) );
		$wpdb->query( $wpdb->prepare( "DELETE FROM `{$this->table_tagmap}` WHERE khash=%s", $k ) );
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		unset( self::$memo[ $key ] );
	}

	public function acquireLock( string $key, int $ms_ttl ): bool {
		// MySQL-based locks are for a future release.
		return true;
	}

	public function releaseLock( string $key ): void {
		/* no-op */
	}
}
