<?php
namespace AIOSEO\Plugin\Pro\Admin;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * The License class to validate/activate/deactivate license keys.
 *
 * @since 4.2.5
 */
class NetworkLicense extends License {
	/**
	 * The Action Scheduler action name for the recurring daily network license check.
	 *
	 * @since 4.9.5.2
	 *
	 * @var string
	 */
	public $licenseCheckAction = 'aioseo_network_license_check';

	/**
	 * The Action Scheduler action name for the expired network license retry check.
	 *
	 * @since 4.9.5.2
	 *
	 * @var string
	 */
	public $licenseCheckExpiredAction = 'aioseo_network_license_check_expired';

	/**
	 * The Action Scheduler action name for resetting AI access tokens across subsites.
	 *
	 * @since 4.9.5.2
	 *
	 * @var string
	 */
	private $resetSubsiteTokensAction = 'aioseo_ai_reset_subsite_tokens';

	/**
	 * Class constructor.
	 *
	 * @since 4.2.5
	 */
	public function __construct() {
		$this->options          = aioseo()->networkOptions;
		$this->internalOptions  = aioseo()->internalNetworkOptions;
		$this->sensitiveOptions = aioseo()->networkSensitiveOptions;
		$this->sensitiveKeyName = 'networkLicenseKey';

		add_action( 'admin_init', [ $this, 'maybeActivateFromConstant' ], 2 );
		add_action( 'admin_init', [ $this, 'scheduleLicenseCheck' ], 3 );
		add_action( $this->licenseCheckAction, [ $this, 'checkLicense' ] );
		add_action( $this->licenseCheckExpiredAction, [ $this, 'checkLicense' ] );
		add_action( $this->resetSubsiteTokensAction, [ $this, 'resetSubsiteTokens' ] );

		include_once ABSPATH . 'wp-admin/includes/plugin.php';
		if (
			is_network_admin() &&
			! is_plugin_active_for_network( plugin_basename( AIOSEO_FILE ) )
		) {
			return;
		}

		// phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized, HM.Security.NonceVerification.Recommended
		if ( ! isset( $_GET['page'] ) || 'aioseo-settings' !== sanitize_text_field( wp_unslash( $_GET['page'] ) ) ) {
			add_action( 'network_admin_notices', [ $this, 'notices' ] );
		}
		// phpcs:enable

		add_action( 'after_plugin_row_' . AIOSEO_PLUGIN_BASENAME, [ $this, 'pluginRowNotice' ] );
		add_action( 'in_plugin_update_message-' . AIOSEO_PLUGIN_BASENAME, [ $this, 'updateRowNotice' ] );
	}

	/**
	 * Activates the network license from the AIOSEO_LICENSE_KEY constant if defined and different from the stored key.
	 *
	 * Deferred to admin_init so the full aioseo() container (actionScheduler, etc.) is ready.
	 *
	 * @since 4.9.6.2
	 *
	 * @return void
	 */
	public function maybeActivateFromConstant() {
		if ( ! defined( 'AIOSEO_LICENSE_KEY' ) || empty( AIOSEO_LICENSE_KEY ) ) {
			return;
		}

		if ( AIOSEO_LICENSE_KEY === $this->sensitiveOptions->get( $this->sensitiveKeyName ) ) {
			return;
		}

		$this->sensitiveOptions->set( $this->sensitiveKeyName, AIOSEO_LICENSE_KEY );
		aioseo()->actionScheduler->unschedule( $this->licenseCheckAction );
		$this->activate();
	}

	/**
	 * Validate the license keys for a multisite setup.
	 *
	 * @since 4.2.5
	 *
	 * @param  array    $domains Domains for activation and deactivation.
	 * @return boolean           Whether or not it was activated.
	 */
	public function multisite( $domains ) {
		aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );

		$this->internalOptions->internal->license->reset(
			[
				'expires',
				'expired',
				'invalid',
				'disabled',
				'activationsError',
				'connectionError',
				'requestError',
				'level',
				'addons',
				'counts',
				'upgradeUrl'
			]
		);

		$licenseKey = aioseo()->networkSensitiveOptions->get( 'networkLicenseKey' );
		if ( empty( $licenseKey ) ) {
			aioseo()->helpers->restoreCurrentBlog();

			return false;
		}

		$site    = aioseo()->helpers->getSite();
		$domains = ! empty( $domains )
			? $domains
			: [
				[
					'domain' => $site->domain,
					'path'   => $site->path
				]
			];

		$response = $this->sendLicenseRequest( 'multisite', $licenseKey, $domains );

		if ( empty( $response ) ) {
			// Something bad happened, error unknown.
			$this->internalOptions->internal->license->connectionError = true;

			aioseo()->helpers->restoreCurrentBlog();

			return false;
		}

		if ( ! empty( $response->error ) ) {
			if ( 'missing-key-or-domain' === $response->error ) {
				$this->internalOptions->internal->license->requestError = true;

				aioseo()->helpers->restoreCurrentBlog();

				return false;
			}

			if ( 'missing-license' === $response->error ) {
				$this->internalOptions->internal->license->invalid = true;

				aioseo()->helpers->restoreCurrentBlog();

				return false;
			}

			if ( 'disabled' === $response->error ) {
				$this->internalOptions->internal->license->disabled = true;

				aioseo()->helpers->restoreCurrentBlog();

				return false;
			}

			if ( 'activations' === $response->error ) {
				$this->internalOptions->internal->license->activationsError = true;

				aioseo()->helpers->restoreCurrentBlog();

				return false;
			}

			if ( 'expired' === $response->error ) {
				$this->internalOptions->internal->license->expires = strtotime( $response->expires );
				$this->internalOptions->internal->license->expired = true;

				aioseo()->helpers->restoreCurrentBlog();

				return false;
			}
		}

		// Something bad happened, error unknown.
		if ( empty( $response->success ) || empty( $response->level ) ) {
			aioseo()->helpers->restoreCurrentBlog();

			return false;
		}

		$this->internalOptions->internal->license->level    = $response->level;
		$this->internalOptions->internal->license->addons   = wp_json_encode( $response->addons );
		$this->internalOptions->internal->license->expires  = strtotime( $response->expires );
		$this->internalOptions->internal->license->features = wp_json_encode( $response->features );

		// Store activation counts if provided.
		if ( ! empty( $response->counts ) ) {
			$this->internalOptions->internal->license->counts = wp_json_encode( $response->counts );
		}

		// Store upgrade URL if provided.
		if ( ! empty( $response->upgradeUrl ) ) {
			$this->internalOptions->internal->license->upgradeUrl = $response->upgradeUrl;
		}

		aioseo()->helpers->restoreCurrentBlog();

		if ( aioseo()->internalOptions->internal->has( 'ai' ) ) {
			$activatedIds   = ! empty( $domains['activate'] ) ? array_column( $domains['activate'], 'blog_id' ) : [];
			$deactivatedIds = ! empty( $domains['deactivate'] ) ? array_column( $domains['deactivate'], 'blog_id' ) : [];
			$blogIds        = array_values( array_filter( array_merge( $activatedIds, $deactivatedIds ) ) );

			$this->scheduleTokenReset( $blogIds );
		}

		$this->clearSitesActiveCache( $domains );

		return true;
	}


	/**
	 * Validate the license key.
	 *
	 * @since 4.2.5
	 *
	 * @param  array   $newDomains New domains to activate.
	 * @return boolean             Whether or not it was activated.
	 */
	public function activate( $newDomains = [] ) {
		aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );

		$this->internalOptions->internal->license->reset(
			[
				'expires',
				'expired',
				'invalid',
				'disabled',
				'activationsError',
				'connectionError',
				'requestError',
				'level',
				'addons',
				'features',
				'counts',
				'upgradeUrl'
			]
		);

		$licenseKey = aioseo()->networkSensitiveOptions->get( 'networkLicenseKey' );
		if ( empty( $licenseKey ) ) {
			aioseo()->helpers->restoreCurrentBlog();

			return false;
		}

		$site    = aioseo()->helpers->getSite();
		$domains = ! empty( $newDomains )
			? $newDomains
			: [
				[
					'domain' => $site->domain,
					'path'   => $site->path
				]
			];

		$response = $this->sendLicenseRequest( 'activate', $licenseKey, $domains );

		if ( empty( $response ) ) {
			// Something bad happened, error unknown.
			$this->internalOptions->internal->license->connectionError = true;

			aioseo()->helpers->restoreCurrentBlog();

			return false;
		}

		if ( ! empty( $response->error ) ) {
			if ( 'missing-key-or-domain' === $response->error ) {
				$this->internalOptions->internal->license->requestError = true;

				aioseo()->helpers->restoreCurrentBlog();

				return false;
			}

			if ( 'missing-license' === $response->error ) {
				$this->internalOptions->internal->license->invalid = true;

				aioseo()->helpers->restoreCurrentBlog();

				return false;
			}

			if ( 'disabled' === $response->error ) {
				$this->internalOptions->internal->license->disabled = true;

				aioseo()->helpers->restoreCurrentBlog();

				return false;
			}

			if ( 'domain-disabled' === $response->error ) {
				$this->internalOptions->internal->license->domainDisabled = true;

				return false;
			}

			if ( 'activations' === $response->error ) {
				$this->internalOptions->internal->license->activationsError = true;

				aioseo()->helpers->restoreCurrentBlog();

				return false;
			}

			if ( 'expired' === $response->error ) {
				$this->internalOptions->internal->license->expires = strtotime( $response->expires );
				$this->internalOptions->internal->license->expired = true;

				aioseo()->helpers->restoreCurrentBlog();

				return false;
			}
		}

		// Something bad happened, error unknown.
		if ( empty( $response->success ) || empty( $response->level ) ) {
			aioseo()->helpers->restoreCurrentBlog();

			return false;
		}

		$this->internalOptions->internal->license->level    = $response->level;
		$this->internalOptions->internal->license->addons   = wp_json_encode( $response->addons );
		$this->internalOptions->internal->license->expires  = strtotime( $response->expires );
		$this->internalOptions->internal->license->features = wp_json_encode( $response->features );

		// Store activation counts if provided.
		if ( ! empty( $response->counts ) ) {
			$this->internalOptions->internal->license->counts = wp_json_encode( $response->counts );
		}

		// Store upgrade URL if provided.
		if ( ! empty( $response->upgradeUrl ) ) {
			$this->internalOptions->internal->license->upgradeUrl = $response->upgradeUrl;
		}

		aioseo()->helpers->restoreCurrentBlog();

		if ( aioseo()->internalOptions->internal->has( 'ai' ) ) {
			$this->scheduleTokenReset();
		}

		$this->clearSitesActiveCache( $newDomains );

		return true;
	}

	/**
	 * Deactivate the license key.
	 *
	 * @since 4.2.5
	 *
	 * @param  array   $domains New domains to activate.
	 * @return boolean          Whether it was deactivated.
	 */
	public function deactivate( $domains = [] ) {
		aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );

		$licenseKey = aioseo()->networkSensitiveOptions->get( 'networkLicenseKey' );
		if ( empty( $licenseKey ) ) {
			aioseo()->helpers->restoreCurrentBlog();

			return false;
		}

		$site    = aioseo()->helpers->getSite();
		$domainsToDeactivate = ! empty( $domains )
			? $domains
			: [
				[
					'domain' => $site->domain,
					'path'   => $site->path
				]
			];

		$response = $this->sendLicenseRequest( 'deactivate', $licenseKey, $domainsToDeactivate );

		if ( empty( $response ) ) {
			// Something bad happened, error unknown.
			$this->internalOptions->internal->license->connectionError = true;

			aioseo()->helpers->restoreCurrentBlog();

			return false;
		}

		if ( ! empty( $response->error ) ) {
			if ( 'missing-key-or-domain' === $response->error || 'not-activated' === $response->error ) {
				$this->internalOptions->internal->license->requestError = true;

				aioseo()->helpers->restoreCurrentBlog();

				return false;
			}

			if ( 'missing-license' === $response->error ) {
				$this->internalOptions->internal->license->invalid = true;

				aioseo()->helpers->restoreCurrentBlog();

				return false;
			}

			if ( 'disabled' === $response->error ) {
				$this->internalOptions->internal->license->disabled = true;

				aioseo()->helpers->restoreCurrentBlog();

				return false;
			}

			if ( 'domain-disabled' === $response->error ) {
				$this->internalOptions->internal->license->domainDisabled = true;

				return false;
			}
		}

		$this->internalOptions->internal->license->reset(
			[
				'expires',
				'expired',
				'invalid',
				'disabled',
				'activationsError',
				'connectionError',
				'requestError',
				'level',
				'addons'
			]
		);

		$this->internalOptions->internal->license->level    = $response->level;
		$this->internalOptions->internal->license->addons   = wp_json_encode( $response->addons );
		$this->internalOptions->internal->license->expires  = strtotime( $response->expires );
		$this->internalOptions->internal->license->features = wp_json_encode( $response->features );

		if ( aioseo()->internalOptions->internal->has( 'ai' ) ) {
			// Update the AI credits.
			aioseo()->internalOptions->internal->ai->accessToken = '';

			// The AI class may not be initialized yet during early admin hooks.
			if ( ! empty( aioseo()->ai ) ) {
				aioseo()->ai->getAccessToken( true );
			}
		}

		aioseo()->seoChecklist->uncompleteCheck( 'licenseActivated' );

		aioseo()->helpers->restoreCurrentBlog();

		if ( aioseo()->internalOptions->internal->has( 'ai' ) ) {
			$this->scheduleTokenReset();
		}

		$this->clearSitesActiveCache( $domains );

		return true;
	}

	/**
	 * Schedules a one-time action to clear AI access tokens for subsites.
	 *
	 * @since 4.9.5.2
	 *
	 * @param  array $blogIds Specific blog IDs to reset. Empty clears all subsites.
	 * @return void
	 */
	private function scheduleTokenReset( $blogIds = [] ) {
		aioseo()->actionScheduler->scheduleSingle( $this->resetSubsiteTokensAction, 0, [ $blogIds ] );
	}

	/**
	 * Resets all AI settings (access token, credits, etc.) for the given subsites (or all subsites if none specified).
	 * Hooked into `aioseo_ai_reset_subsite_tokens` action hook.
	 *
	 * Each affected subsite will fetch a fresh token and credits on its next admin load via
	 * the scheduled `aioseo_ai_get_access_token` action.
	 *
	 * @since 4.9.5.2
	 *
	 * @param  array $blogIds Specific blog IDs to reset. Empty clears all subsites.
	 * @return void
	 */
	public function resetSubsiteTokens( $blogIds = [] ) {
		if ( ! is_multisite() ) {
			return;
		}

		$sites = ! empty( $blogIds )
			? $blogIds
			: get_sites( [
				'fields' => 'ids',
				'number' => 0
			] );

		$optionName = 'aioseo_options_internal';
		foreach ( $sites as $blogId ) {
			$json = get_blog_option( (int) $blogId, $optionName );
			if ( ! $json ) {
				continue;
			}

			$options = json_decode( $json, true );
			if ( empty( $options['internal']['ai'] ) ) {
				continue;
			}

			$options['internal']['ai'] = [];
			update_blog_option( (int) $blogId, $optionName, wp_json_encode( $options ) );
		}

		$sensitiveOptionName = 'aioseo_sensitive_options';
		foreach ( $sites as $blogId ) {
			$json = get_blog_option( (int) $blogId, $sensitiveOptionName );
			if ( ! $json ) {
				continue;
			}

			$options = json_decode( $json, true );
			if ( empty( $options['aiAccessToken'] ) ) {
				continue;
			}

			$options['aiAccessToken'] = '';
			update_blog_option( (int) $blogId, $sensitiveOptionName, wp_json_encode( $options ) );
		}
	}

	/**
	 * Validates the stored license within the main network site context.
	 *
	 * Overrides the parent method to ensure that `getSite()` returns the main
	 * network site's domain, not a random subsite that Action Scheduler may
	 * execute from.
	 *
	 * @since 4.9.5.2
	 *
	 * @return void
	 */
	public function checkLicense() {
		aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );

		parent::checkLicense();

		aioseo()->helpers->restoreCurrentBlog();
	}

	/**
	 * Checks to see if the current license is expired.
	 *
	 * @since 4.2.5
	 *
	 * @return bool True if expired, false if not.
	 */
	public function isExpired() {
		if ( ! aioseo()->networkSensitiveOptions->hasValue( 'networkLicenseKey' ) ) {
			return false;
		}

		if ( $this->internalOptions->internal->license->expired ) {
			return true;
		}

		$expires = $this->internalOptions->internal->license->expires;

		return 0 !== $expires && $expires < time();
	}

	/**
	 * Checks to see if the current license is disabled.
	 *
	 * @since 4.2.5
	 *
	 * @return bool True if disabled, false if not.
	 */
	public function isDisabled() {
		if ( ! aioseo()->networkSensitiveOptions->hasValue( 'networkLicenseKey' ) ) {
			return false;
		}

		return $this->internalOptions->internal->license->disabled;
	}

	/**
	 * Checks to see if the current license is invalid.
	 *
	 * @since 4.2.5
	 *
	 * @return bool True if invalid, false if not.
	 */
	public function isInvalid() {
		if ( ! aioseo()->networkSensitiveOptions->hasValue( 'networkLicenseKey' ) ) {
			return false;
		}

		return $this->internalOptions->internal->license->invalid;
	}

	/**
	 * Checks to see if the current license is disabled.
	 *
	 * @since 4.2.5
	 *
	 * @param  \WP_Site $site The site to check if the the license is active on.
	 * @return bool           True if disabled, false if not.
	 */
	public function isActive( $site = null ) {
		if ( ! aioseo()->networkSensitiveOptions->hasValue( 'networkLicenseKey' ) ) {
			return false;
		}

		if ( ! $this->isSiteActive( $site ) ) {
			return false;
		}

		return ! $this->isExpired() && ! $this->isDisabled() && ! $this->isInvalid();
	}

	/**
	 * Get the license level for the activated license.
	 *
	 * @since 4.2.5
	 *
	 * @param  \WP_Site $site The site to check if the the license is active on.
	 * @return string         The license level.
	 */
	public function getLicenseLevel( $site = null ) {
		if ( ! aioseo()->networkSensitiveOptions->hasValue( 'networkLicenseKey' ) ) {
			return 'Unknown';
		}

		if ( ! $this->isSiteActive( $site ) ) {
			return 'Unknown';
		}

		return $this->internalOptions->internal->license->level;
	}

	/**
	 * Checks if the current site is licensed at the network level.
	 *
	 * @since 4.2.5
	 *
	 * @return bool True if licensed at the network level.
	 */
	public function isNetworkLicensed() {
		// If we are already locally activated, then no it's not network licensed.
		if ( aioseo()->license->isActive() ) {
			return false;
		}

		if ( $this->isActive() ) {
			return true;
		}

		return false;
	}

	/**
	 * Checks if a given site (or the current one) is active.
	 *
	 * @since   4.2.5
	 * @version 4.4.0
	 *
	 * @param  \WP_Site $site The site to check.
	 * @return bool           True if active, false if not.
	 */
	public function isSiteActive( $site = null ) {
		if ( ! aioseo()->networkSensitiveOptions->hasValue( 'networkLicenseKey' ) ) {
			return false;
		}

		if ( empty( $site ) ) {
			$site = \WP_Site::get_instance( get_current_blog_id() );
		}

		$urlHash  = sha1( $site->domain . $site->path );
		$cacheKey = "site_active_{$urlHash}";
		$isActive = aioseo()->core->cache->get( $cacheKey );
		if ( null !== $isActive ) {
			return $isActive;
		}

		$licenseKey = aioseo()->networkSensitiveOptions->get( 'networkLicenseKey' );
		$response   = $this->sendLicenseRequest( 'activated', $licenseKey, [ $site ] );
		if ( ! empty( $response->error ) || empty( $response->all_activations_and_paths ) ) {
			aioseo()->core->cache->update( $cacheKey, false, DAY_IN_SECONDS );

			return false;
		}

		$active = false;
		foreach ( $response->all_activations_and_paths as $activeSite ) {
			if ( $site->domain === $activeSite->domain && $site->path === $activeSite->path ) {
				$active = true;
				break;
			}

			if ( ! empty( $site->aliases ) ) {
				foreach ( $site->aliases as $alias ) {
					if ( $activeSite->domain === $alias['domain'] ) {
						$active = true;
						break;
					}
				}
			}
		}

		aioseo()->core->cache->update( $cacheKey, $active, DAY_IN_SECONDS );

		return $active;
	}

	/**
	 * Checks if the given sites are activated.
	 *
	 * @since 4.4.0
	 *
	 * @param  array $domains The domains to check.
	 * @return array          The domains with the active status.
	 */
	public function areSitesActive( $domains ) {
		// Force domains to be objects.
		$domains = json_decode( wp_json_encode( $domains ) );

		$fullDomains = array_map( function( $domain ) {
			return $domain->domain . $domain->path;
		}, $domains );
		$domainsHash = sha1( implode( ',', $fullDomains ) );
		$cacheKey    = "sites_active_{$domainsHash}";
		$areActive   = aioseo()->core->cache->get( $cacheKey );
		if ( null !== $areActive ) {
			return $areActive;
		}

		$licenseKey = aioseo()->networkSensitiveOptions->get( 'networkLicenseKey' );
		$response   = $this->sendLicenseRequest( 'activated', $licenseKey, $domains );
		if ( ! empty( $response->error ) || empty( $response->all_activations_and_paths ) ) {
			aioseo()->core->cache->update( $cacheKey, [], HOUR_IN_SECONDS );

			return [];
		}

		$activeSites = [];
		foreach ( $domains as $domain ) {
			foreach ( $response->all_activations_and_paths as $activeSite ) {
				if ( $domain->domain === $activeSite->domain && $domain->path === $activeSite->path ) {
					$activeSites[] = $activeSite;
					break;
				}

				if ( ! empty( $domain->aliases ) ) {
					foreach ( $domain->aliases as $alias ) {
						if ( $activeSite->domain === $alias['domain'] ) {
							$activeSites[] = $activeSite;
							break;
						}
					}
				}
			}
		}

		aioseo()->core->cache->update( $cacheKey, $activeSites, HOUR_IN_SECONDS );

		return $activeSites;
	}

	/**
	 * Adds a notice to the update row for unlicensed users.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	public function updateRowNotice() {
		if ( $this->isActive() ) {
			return;
		}

		$this->outputUpdateRowNotice();
	}

	/**
	 * Add row to Plugins page with licensing information, if license key is invalid or not found.
	 *
	 * @since 4.2.5
	 *
	 * @return void
	 */
	public function pluginRowNotice() {
		// Don't output the notice on subsites since to prevent duplicate notices.
		if ( $this->isActive() || ! is_main_site() ) {
			return;
		}

		$this->outputPluginRowNotice();
	}

	/**
	 * Clears the active site(s) cache.
	 *
	 * @since 4.4.0
	 *
	 * @return void
	 */
	private function clearSitesActiveCache( $domains = [] ) {
		if ( $this->cacheTableHasNameColumn() ) {
			$cacheTableName = aioseo()->core->db->prefix . 'aioseo_cache';
			aioseo()->core->db->execute( "DELETE FROM $cacheTableName WHERE `name` LIKE 'sites_active_%'" );
		}

		$allDomains = [];
		if ( isset( $domains['activate'] ) ) {
			$allDomains = array_merge( $allDomains, $domains['activate'] );
		}

		if ( isset( $domains['deactivate'] ) ) {
			$allDomains = array_merge( $allDomains, $domains['deactivate'] );
		}

		foreach ( $allDomains as $domain ) {
			aioseo()->helpers->switchToBlog( $domain['blog_id'] );

			if ( $this->cacheTableHasNameColumn() ) {
				$cacheTableName = aioseo()->core->db->prefix . 'aioseo_cache';
				aioseo()->core->db->execute( "DELETE FROM $cacheTableName WHERE `name` LIKE 'site_active_%'" );
			}

			aioseo()->helpers->restoreCurrentBlog();
		}
	}

	/**
	 * Checks if the current site's cache table has the 'name' column.
	 *
	 * Uses raw SQL to avoid circular dependencies with the Cache class,
	 * which may not be initialized for subsites that haven't been migrated yet.
	 *
	 * @since 4.9.7
	 *
	 * @return bool Whether the cache table has the 'name' column.
	 */
	private function cacheTableHasNameColumn() {
		global $wpdb;
		$tableName = $wpdb->prefix . 'aioseo_cache';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$tableExists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tableName ) );
		if ( ! $tableExists ) {
			return false;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return (bool) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COLUMN_NAME
				FROM INFORMATION_SCHEMA.COLUMNS
				WHERE TABLE_SCHEMA = DATABASE()
				AND TABLE_NAME = %s
				AND COLUMN_NAME = 'name'",
				$tableName
			)
		);
	}
}