<?php
/**
 * Storage scope: the single place that decides site-scoped vs network-scoped.
 *
 * ---------------------------------------------------------------------------
 * WHY EVERY PIECE OF UPGRADE PILOT STATE IS NETWORK-SCOPED ON MULTISITE
 *
 * On a WordPress network there is exactly ONE copy of each plugin's files, in
 * wp-content/plugins, shared by every subsite. Upgrade Pilot's entire subject
 * matter is those files and their public WordPress.org record:
 *
 *   - who authors a plugin, and whether that changed;
 *   - whether it was closed on WordPress.org;
 *   - what its PHP code does;
 *   - whether its automatic updates are held.
 *
 * Every one of those facts is a property of the network, not of a subsite. Two
 * subsites cannot disagree about who wrote a plugin, and they cannot run
 * different versions of the same file.
 *
 * So all of it lives at network scope: one set of tables (base_prefix), one
 * settings record, one freeze list, one API cache. The consequences are not
 * cosmetic:
 *
 *   - CORRECTNESS. A per-subsite freeze list would be a lie. The automatic
 *     updater rewrites the shared file; there is no version of that action that
 *     applies to subsite 3 and not subsite 4. A freeze that cannot be honoured
 *     is worse than no freeze, so freeze state is network-scoped and only a
 *     super admin can set it.
 *   - LOAD. The WordPress.org record for a slug is identical on every subsite.
 *     Caching it per subsite would multiply our API traffic by the number of
 *     subsites, for byte-identical answers. Network-scoped caching is what turns
 *     a 500-subsite network from ~20,000 requests a day into one.
 *
 * What genuinely IS per-subsite is which plugins are ACTIVE there. That single
 * fact is what the Pro network rollup reports on, and it is read live from each
 * subsite rather than stored.
 *
 * On single-site WordPress every method here degrades to the ordinary option
 * and transient functions, so there is one code path and no branching at the
 * call sites.
 * ---------------------------------------------------------------------------
 *
 * @package Upgrade_Pilot
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class UPilot_Store
 */
class UPilot_Store {

	/**
	 * Are we operating at network scope?
	 *
	 * @return bool
	 */
	public static function is_network() {
		return is_multisite();
	}

	/**
	 * Read a plugin option.
	 *
	 * @param string $key           Option name.
	 * @param mixed  $default_value Default.
	 * @return mixed
	 */
	public static function get_option( $key, $default_value = false ) {
		return self::is_network()
			? get_site_option( $key, $default_value )
			: get_option( $key, $default_value );
	}

	/**
	 * Write a plugin option.
	 *
	 * @param string $key      Option name.
	 * @param mixed  $value    Value.
	 * @param bool   $autoload Autoload (single site only; network options have no autoload).
	 * @return bool
	 */
	public static function update_option( $key, $value, $autoload = false ) {
		return self::is_network()
			? update_site_option( $key, $value )
			: update_option( $key, $value, $autoload );
	}

	/**
	 * Delete a plugin option.
	 *
	 * @param string $key Option name.
	 * @return bool
	 */
	public static function delete_option( $key ) {
		return self::is_network()
			? delete_site_option( $key )
			: delete_option( $key );
	}

	/**
	 * Atomic test-and-set, used for the scanner's single-flight lock.
	 *
	 * IMPORTANT: this deliberately does NOT use add_site_option() on multisite.
	 * The wp_sitemeta table has no unique index on meta_key, so add_site_option()
	 * is a get-then-insert with a race between the two — two concurrent workers
	 * can both "acquire" the same lock. wp_options DOES carry a unique index on
	 * option_name, so add_option() fails atomically for the loser of the race.
	 *
	 * We therefore always place the lock in the MAIN SITE's wp_options table,
	 * which gives one network-wide lock with real atomicity.
	 *
	 * @param string $key   Option name.
	 * @param string $value Value.
	 * @return bool True if this caller created the option (i.e. won the lock).
	 */
	public static function add_option_atomic( $key, $value ) {
		if ( ! self::is_network() ) {
			return add_option( $key, $value, '', false );
		}
		switch_to_blog( get_main_site_id() );
		$won = add_option( $key, $value, '', false );
		restore_current_blog();
		return $won;
	}

	/**
	 * Read the atomic lock value.
	 *
	 * @param string $key Option name.
	 * @return mixed
	 */
	public static function get_option_atomic( $key ) {
		if ( ! self::is_network() ) {
			return get_option( $key );
		}
		switch_to_blog( get_main_site_id() );
		$value = get_option( $key );
		restore_current_blog();
		return $value;
	}

	/**
	 * Overwrite the atomic lock value (stale-lock takeover).
	 *
	 * @param string $key   Option name.
	 * @param string $value Value.
	 * @return bool
	 */
	public static function update_option_atomic( $key, $value ) {
		if ( ! self::is_network() ) {
			return update_option( $key, $value, false );
		}
		switch_to_blog( get_main_site_id() );
		$ok = update_option( $key, $value, false );
		restore_current_blog();
		return $ok;
	}

	/**
	 * Release the atomic lock.
	 *
	 * @param string $key Option name.
	 * @return bool
	 */
	public static function delete_option_atomic( $key ) {
		if ( ! self::is_network() ) {
			return delete_option( $key );
		}
		switch_to_blog( get_main_site_id() );
		$ok = delete_option( $key );
		restore_current_blog();
		return $ok;
	}

	/**
	 * Read a plugin transient.
	 *
	 * @param string $key Transient name.
	 * @return mixed
	 */
	public static function get_transient( $key ) {
		return self::is_network() ? get_site_transient( $key ) : get_transient( $key );
	}

	/**
	 * Write a plugin transient.
	 *
	 * @param string $key   Transient name.
	 * @param mixed  $value Value.
	 * @param int    $ttl   TTL in seconds.
	 * @return bool
	 */
	public static function set_transient( $key, $value, $ttl ) {
		return self::is_network()
			? set_site_transient( $key, $value, $ttl )
			: set_transient( $key, $value, $ttl );
	}

	/**
	 * Delete a plugin transient.
	 *
	 * @param string $key Transient name.
	 * @return bool
	 */
	public static function delete_transient( $key ) {
		return self::is_network() ? delete_site_transient( $key ) : delete_transient( $key );
	}

	/**
	 * Table prefix for Upgrade Pilot's own tables.
	 *
	 * base_prefix on multisite: one table set for the whole network, because the
	 * data describes network-shared plugin files. Per-subsite tables would store
	 * N identical copies of the same rows.
	 *
	 * @return string
	 */
	public static function table_prefix() {
		global $wpdb;
		return self::is_network() ? $wpdb->base_prefix : $wpdb->prefix;
	}

	/**
	 * Fully-qualified name of one of our tables.
	 *
	 * @param string $name Bare table name (snapshots, events, scan_queue, findings, site_status).
	 * @return string
	 */
	public static function table( $name ) {
		return self::table_prefix() . 'upilot_' . $name;
	}

	/**
	 * Capability required to view and change Upgrade Pilot.
	 *
	 * On a network, everything Upgrade Pilot manages is network-wide, so it is a
	 * super-admin tool. A subsite administrator cannot install, update or freeze
	 * a plugin on a network anyway, so gating on manage_network_options loses
	 * them nothing and prevents one subsite changing settings for all the others.
	 *
	 * @return string
	 */
	public static function cap() {
		return self::is_network() ? 'manage_network_options' : 'manage_options';
	}

	/**
	 * Admin URL for one of our screens.
	 *
	 * @param string $path Relative admin path.
	 * @return string
	 */
	public static function admin_url( $path ) {
		return self::is_network() ? network_admin_url( $path ) : admin_url( $path );
	}

	/**
	 * Where alerts should go by default.
	 *
	 * @return string
	 */
	public static function default_admin_email() {
		return self::is_network()
			? (string) get_site_option( 'admin_email' )
			: (string) get_option( 'admin_email' );
	}

	/**
	 * Human name of the thing being monitored (network name, or site name).
	 *
	 * @return string
	 */
	public static function display_name() {
		if ( self::is_network() ) {
			$name = (string) get_site_option( 'site_name' );
			if ( '' !== $name ) {
				return $name;
			}
		}
		return (string) get_option( 'blogname' );
	}

	/**
	 * Is the current blog the one that owns scheduled work?
	 *
	 * WP-Cron is per-site. If Upgrade Pilot scheduled its daily jobs on every
	 * subsite of a network, a 500-site network would run 500 identical snapshots
	 * against one shared dataset. Scheduled work therefore belongs to the main
	 * site only.
	 *
	 * @return bool
	 */
	public static function is_cron_site() {
		return ! self::is_network() || get_current_blog_id() === get_main_site_id();
	}
}
