<?php

/**
 * Class FeedCache
 *
 * @since 1.0
 */

namespace SmashBalloon\Reviews\Common;

class FeedCache {
	public const CACHES_TABLE_NAME = 'sbr_feed_caches';

	public const CACHE_KEY = 'sbr_feed_';

	/**
	 * @var string
	 */
	protected $feed_id;

	/**
	 * @var string
	 */
	protected $suffix;

	/**
	 * @var int
	 */
	private $cache_time;

	/**
	 * @var bool
	 */
	private $is_expired = true;

	/**
	 * @var array
	 */
	private $posts;

	/**
	 * @var array
	 */
	private $header;

	/**
	 * @var array
	 */
	private $posts_backup;

	/**
	 * @var array
	 */
	private $header_backup;

	/**
	 * @var array
	 */
	private $errors;



	/**
	 * SBI_Cache constructor. Set the feed id, cache key, legacy
	 *
	 * @param string $feed_id
	 * @param int $cache_time
	 *
	 * @since 6.0
	 */
	public function __construct($feed_id, $cache_time = 0)
	{
		$this->cache_time = (int) $cache_time;
		$this->feed_id    = str_replace('*', '', $feed_id);
		$this->suffix     = '';

		if (is_admin()) {
			$this->feed_id .= $this->maybe_customizer_suffix();
		}
	}

	/**
	 * Set all caches based on available data.
	 *
	 * @since 6.0
	 */
	public function retrieve_and_set()
	{
		$expired         = true;
		$existing_caches = $this->query_feed_caches();

		foreach ($existing_caches as $cache) {
			switch ($cache['cache_key']) {
				case 'posts':
					$this->posts = $cache['cache_value'];
					if (strtotime($cache['last_updated']) > time() - $this->cache_time) {
						$expired = false;
					}

					if (empty($cache['cache_value'])) {
						$expired = true;
					}

					break;
				case 'header':
					$this->header = $cache['cache_value'];
					break;
				case 'posts_backup' . $this->suffix:
					$this->posts_backup = $cache['cache_value'];
					break;
				case 'header_backup' . $this->suffix:
					$this->header_backup = $cache['cache_value'];
					break;
				case 'errors' . $this->suffix:
					$this->errors = $cache['cache_value'];
					break;
			}
		}

		$this->is_expired = $expired;
		if ($this->cache_time < 1) {
			$this->is_expired = true;
		}
	}

	/**
	 * Whether or not the cache needs to be refreshed
	 *
	 * @param string $cache_type
	 *
	 * @return bool
	 *
	 * @since 6.0
	 */
	public function is_expired($cache_type = 'posts')
	{
		if ($cache_type !== 'posts') {
			$cache = $this->get($cache_type);

			return ( empty($cache) || $this->is_expired );
		}

		return $this->is_expired;
	}

	/**
	 * Get data currently stored in the database for the type
	 *
	 * @param string $type
	 *
	 * @return string
	 *
	 * @since 6.0
	 */
	public function get($type)
	{
		$return = array();
		switch ($type) {
			case 'posts':
				$return = $this->posts;
				break;
			case 'posts' . $this->suffix:
				$return = $this->posts_page;
				break;
			case 'header':
				$return = $this->header;
				break;
			case 'posts_backup':
				$return = $this->posts_backup;
				break;
			case 'header_backup':
				$return = $this->header_backup;
				break;
			case 'errors':
				$return = $this->errors;
				break;
		}

		return $return;
	}

	/**
	 * @param string $type
	 * @param array $cache_value
	 *
	 * @since 6.0
	 */
	public function set($type, $cache_value)
	{
		switch ($type) {
			case 'posts':
				$this->posts = $cache_value;
				break;
			case 'posts' . $this->suffix:
				$this->posts_page = $cache_value;
				break;
			case 'header':
				$this->header = $cache_value;
				break;
			case 'posts_backup':
				$this->posts_backup = $cache_value;
				break;
			case 'header_backup':
				$this->header_backup = $cache_value;
				break;
			case 'errors':
				$this->errors = $cache_value;
				break;
		}
	}

	/**
	 * Update a single cache with new data. Try to accept any data and convert it
	 * to JSON if needed
	 *
	 * @param string $cache_type
	 * @param array|object|string $cache_value
	 * @param bool $include_backup
	 * @param bool $cron_update
	 *
	 * @return int
	 *
	 * @since 6.0
	 */
	public function update_or_insert($cache_type, $cache_value, $include_backup = true, $cron_update = true)
	{
		$this->clear_wp_cache();
		if ($cache_type !== 'posts' && $cache_type !== 'header') {
			$cron_update = false;
		}

		if (strpos($this->feed_id, '_CUSTOMIZER') !== false) {
			$cron_update = false;
		}

		// When updating regular cache with posts, clear stale Customizer cache
		// This prevents race condition where Customizer cached empty results before posts were fetched
		if ($cache_type === 'posts' && strpos($this->feed_id, '_CUSTOMIZER') === false) {
			// Handle both array and string cache values
			$has_posts = false;
			if (is_array($cache_value) || is_object($cache_value)) {
				$has_posts = ! empty($cache_value);
			} elseif (is_string($cache_value)) {
				$has_posts = ! empty($cache_value) && $cache_value !== '[]' && $cache_value !== 'null';
			}

			if ($has_posts) {
				$this->clear_customizer_posts_cache();
			}
		}

		$cache_key = $cache_type . $this->suffix;

		$this->set($cache_key, $cache_value);

		if (is_array($cache_value) || is_object($cache_value)) {
			$cache_value = wp_json_encode($cache_value);
		}

		global $wpdb;
		$cache_table_name = $wpdb->prefix . self::CACHES_TABLE_NAME;

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- Table name is safely constructed from $wpdb->prefix
		$sql = $wpdb->prepare(
			"
			SELECT * FROM $cache_table_name
			WHERE feed_id = %s
			AND cache_key = %s",
			$this->feed_id,
			$cache_key
		);

		$existing = $wpdb->get_results($sql, ARRAY_A);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
		$data     = array();
		$where    = array();
		$format   = array();

		$data['cache_value'] = $cache_value;
		$format[]            = '%s';

		$data['last_updated'] = date('Y-m-d H:i:s');
		$format[]             = '%s';

		if (! empty($existing[0])) {
			$where['feed_id'] = $this->feed_id;
			$where_format[]   = '%s';

			$where['cache_key'] = $cache_key;
			$where_format[]     = '%s';

			$affected = $wpdb->update($cache_table_name, $data, $where, $format, $where_format);
		} else {
			$data['cache_key'] = $cache_key;
			$format[]          = '%s';

			$data['cron_update'] = $cron_update === true ? 'yes' : '';
			$format[]            = '%s';

			$data['feed_id'] = $this->feed_id;
			$format[]        = '%s';

			$affected = $wpdb->insert($cache_table_name, $data, $format);
		}

		return $affected;
	}

	/**
	 * Tasks to do after a new set of posts are retrieved
	 *
	 * @since 6.0
	 */
	public function after_new_posts_retrieved()
	{
		$this->clear('all');
	}

	/**
	 * Resets caches after they expire
	 *
	 * @param string $type
	 *
	 * @return bool|false|int
	 *
	 * @since 6.0
	 */
	public function clear($type)
	{
		$this->clear_wp_cache();

		global $wpdb;
		$cache_table_name = $wpdb->prefix . self::CACHES_TABLE_NAME;

		$feed_id = str_replace(array( '_CUSTOMIZER', '_CUSTOMIZER_MODMODE' ), '', $this->feed_id);

		if ($type === 'all') {
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safely constructed from $wpdb->prefix
			$affected = $wpdb->query(
				$wpdb->prepare(
					"UPDATE $cache_table_name
				SET cache_value = ''
				WHERE feed_id = %s
				AND cache_key NOT IN ( 'posts', 'posts_backup', 'header_backup' );",
					$feed_id
				)
			);

			$affected = $wpdb->query(
				$wpdb->prepare(
					"UPDATE $cache_table_name
				SET cache_value = ''
				WHERE feed_id = %s",
					$feed_id . '_CUSTOMIZER'
				)
			);
			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

			$mod_mode_where = esc_sql($feed_id) . '_CUSTOMIZER_MODMODE%';
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safely constructed from $wpdb->prefix
			$affected       = $wpdb->query(
				$wpdb->prepare(
					"UPDATE $cache_table_name
				SET cache_value = ''
				WHERE feed_id like %s",
					$mod_mode_where
				)
			);
			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		} else {
			$data   = array( 'cache_value' => '' );
			$format = array( '%s' );

			$where['feed_id'] = $feed_id;
			$where_format[]   = '%s';

			$where['cache_key'] = $type . $this->suffix;
			$where_format[]     = '%s';

			$affected = $wpdb->update($cache_table_name, $data, $where, $format, $where_format);

			$where['feed_id'] = $feed_id . '_CUSTOMIZER';

			$affected = $wpdb->update($cache_table_name, $data, $where, $format, $where_format);

			$where['feed_id'] = $feed_id . '_CUSTOMIZER_MODMODE';

			$affected = $wpdb->update($cache_table_name, $data, $where, $format, $where_format);
		}

		return $affected;
	}

	public function get_customizer_cache()
	{
		if (strpos($this->feed_id, '_CUSTOMIZER') === false) {
			$feed_id = $this->feed_id . '_CUSTOMIZER';
		} else {
			$feed_id = $this->feed_id;
		}
		global $wpdb;
		$cache_table_name = $wpdb->prefix . self::CACHES_TABLE_NAME;

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- Table name is safely constructed from $wpdb->prefix
		$sql     = $wpdb->prepare(
			"
			SELECT * FROM $cache_table_name
			WHERE feed_id = %s
			AND cache_key = 'posts'",
			$feed_id
		);
		$results = $wpdb->get_results($sql, ARRAY_A);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared

		$return = array();
		if (! empty($results[0])) {
			$return = $results[0]['cache_value'];
			$return = json_decode($return, true);

			$return = isset($return['data']) ? $return['data'] : array();
		}

		return $return;
	}

	/**
	 * Get all available caches from the feed cache table.
	 *
	 * @return array
	 *
	 * @since 6.0
	 */
	private function query_feed_caches()
	{
		$feed_cache = wp_cache_get($this->get_wp_cache_key());
		if (false === $feed_cache) {
			global $wpdb;
			$cache_table_name = $wpdb->prefix . self::CACHES_TABLE_NAME;

			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- Table name is safely constructed from $wpdb->prefix
			$sql = $wpdb->prepare(
				"
			SELECT * FROM $cache_table_name
			WHERE feed_id = %s",
				$this->feed_id
			);

			$feed_cache = $wpdb->get_results($sql, ARRAY_A);
			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared

			wp_cache_set($this->get_wp_cache_key(), $feed_cache);
		}

		return $feed_cache;
	}

	/**
	 * Delete the wp_cache
	 *
	 * @since 6.0
	 */
	private function clear_wp_cache()
	{
		wp_cache_delete($this->get_wp_cache_key());
	}

	/**
	 * Clear the Customizer posts cache when regular cache is updated.
	 *
	 * This prevents stale empty cache in Customizer when posts are fetched
	 * after the Customizer was already opened (race condition fix).
	 *
	 * @since 2.4.5
	 */
	private function clear_customizer_posts_cache()
	{
		global $wpdb;
		$cache_table_name = $wpdb->prefix . self::CACHES_TABLE_NAME;
		$customizer_feed_id = $this->feed_id . '_CUSTOMIZER';

		// Clear standard Customizer cache
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safely constructed from $wpdb->prefix
		$wpdb->query(
			$wpdb->prepare(
				"UPDATE $cache_table_name
				SET cache_value = ''
				WHERE feed_id = %s
				AND cache_key = 'posts'",
				$customizer_feed_id
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		// Clear MODMODE Customizer caches (with any offset)
		$mod_mode_pattern = esc_sql((string) $this->feed_id) . '_CUSTOMIZER_MODMODE%';
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safely constructed from $wpdb->prefix
		$wpdb->query(
			$wpdb->prepare(
				"UPDATE $cache_table_name
				SET cache_value = ''
				WHERE feed_id LIKE %s
				AND cache_key = 'posts'",
				$mod_mode_pattern
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		// Also clear WP object cache for the Customizer feed
		wp_cache_delete(self::CACHE_KEY . $customizer_feed_id . '_');
	}

	/**
	 * Key used to get the wp cache key
	 *
	 * @return string
	 *
	 * @since 6.0
	 */
	private function get_wp_cache_key()
	{
		return self::CACHE_KEY . $this->feed_id . '_' . $this->suffix;
	}



	/**
	 * Add suffix to cache keys used in the customizer
	 *
	 * @return string
	 *
	 * @since 6.0
	 */
	private function maybe_customizer_suffix()
	{
		$additional_suffix = '';
		// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- Used only for cache key generation
		$in_customizer     = ! empty($_POST['previewSettings']) || ( isset($_GET['page']) && $_GET['page'] === 'sbr' );

		if ($in_customizer) {
			$additional_suffix .= '_CUSTOMIZER';

			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Used only for cache key generation
			if (! empty($_POST['moderationShoppableMode'])) {
				$additional_suffix .= '_MODMODE';
				// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Used only for cache key generation
				$offset             = ! empty($_POST['moderationShoppableModeOffset']) ? intval($_POST['moderationShoppableModeOffset']) : '';
				$additional_suffix .= $offset;
			}
		}

		return $additional_suffix;
	}

	public function update_last_updated()
	{
		global $wpdb;
		$cache_table_name = $wpdb->prefix . self::CACHES_TABLE_NAME;

		$data         = array();
		$format       = array();
		$where_format = array();

		$data['last_updated'] = date('Y-m-d H:i:s');
		$format[]             = '%s';

		$where['feed_id'] = $this->feed_id;
		$where_format[]   = '%s';

		$where['cache_key'] = 'posts';
		$where_format[]     = '%s';

		$affected = $wpdb->update($cache_table_name, $data, $where, $format, $where_format);

		$data         = array();
		$format       = array();
		$where_format = array();

		$data['last_updated'] = date('Y-m-d H:i:s');
		$format[]             = '%s';

		$where['feed_id'] = $this->feed_id;
		$where_format[]   = '%s';

		$where['cache_key'] = 'header';
		$where_format[]     = '%s';

		$affected = $wpdb->update($cache_table_name, $data, $where, $format, $where_format);

		return $affected;
	}

	/**
	 * Get active/all cache count.
	 *
	 * @param bool $active when set to true only items updated in the last months are returned.
	 *
	 * @return int
	 */
	public function get_cache_count($active = false)
	{
		global $wpdb;
		$cache_table_name = $wpdb->prefix . self::CACHES_TABLE_NAME;
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safely constructed from $wpdb->prefix
		$query            = "SELECT COUNT(DISTINCT feed_id, cache_key) as cache_count FROM $cache_table_name WHERE feed_id Not Like '%_CUSTOMIZER%'";

		if ($active === true) {
			$query .= " AND feed_id Not Like '%_MODMODE%' AND last_updated >= DATE_SUB(NOW(), INTERVAL 1 MONTH)";
		}

		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Query is safely constructed above
		$sql    = $wpdb->prepare($query);
		$caches = $wpdb->get_results($sql);
		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared

		if (! empty($caches)) {
			return $caches[0]->cache_count;
		}

		return 0;
	}

}
