<?php
if ( ! defined( 'ABSPATH' ) ) {
	die( 'You are not allowed to call this page directly.' );
}

class FrmFormApi {

	/**
	 * @var string
	 */
	protected $license = '';

	/**
	 * @var string
	 */
	protected $cache_key = '';

	/**
	 * @var string
	 */
	protected $cache_timeout = '+6 hours';

	/**
	 * The number of days an add-on is new.
	 *
	 * @var int
	 */
	protected $new_days = 90;

	/**
	 * If true, calls to get_api_info will bypass cache.
	 * This is set true by calling force_api_request.
	 *
	 * @var bool
	 */
	protected $force = false;

	/**
	 * @since 3.06
	 *
	 * @param string|null $license The license key.
	 *
	 * @return void
	 */
	public function __construct( $license = null ) {
		$this->set_license( $license );
		$this->set_cache_key();
	}

	/**
	 * @since 3.06
	 *
	 * @param string|null $license The license key.
	 *
	 * @return void
	 */
	private function set_license( $license ) {
		if ( $license === null ) {
			$edd_update = $this->get_pro_updater();

			if ( $edd_update ) {
				$license = $edd_update->license;
			}
		}
		$this->license = $license;
	}

	/**
	 * @since 3.06
	 *
	 * @return string
	 */
	public function get_license() {
		return $this->license;
	}

	/**
	 * @since 3.06
	 *
	 * @return void
	 */
	protected function set_cache_key() {
		$this->cache_key = 'frm_addons_l' . ( $this->license ? md5( $this->license ) : '' );
	}

	/**
	 * @since 3.06
	 *
	 * @return string
	 */
	public function get_cache_key() {
		return $this->cache_key;
	}

	/**
	 * Flag the force property as true, so the next API request bypasses cache.
	 * This is used to pull API data for change logs, which are excluded from the cached data.
	 *
	 * @since 6.28
	 *
	 * @return void
	 */
	public function force_api_request() {
		$this->force = true;
	}

	/**
	 * @since 3.06
	 *
	 * @return array
	 */
	public function get_api_info() {
		$url = $this->api_url();

		if ( $this->license ) {
			$url .= '?l=' . urlencode( base64_encode( $this->license ) );
		}

		if ( $this->force ) {
			$addons      = false;
			$this->force = false;
		} else {
			$addons = $this->get_cached();
		}

		if ( is_array( $addons ) ) {
			return $addons;
		}

		if ( $this->is_running() ) {
			// If there's no saved cache, we'll need to wait for the current request to finish.
			return array();
		}

		$this->set_running();

		// We need to know the version number to allow different downloads.
		$agent = 'formidable/' . FrmAppHelper::plugin_version();

		if ( class_exists( 'FrmProDb' ) ) {
			$agent = 'formidable-pro/' . FrmProDb::$plug_version;
		}

		$response = wp_remote_get(
			$url,
			array(
				'timeout'    => 25,
				'user-agent' => $agent . '; ' . get_bloginfo( 'url' ),
			)
		);

		if ( is_array( $response ) && ! is_wp_error( $response ) ) {
			$addons = $response['body'] ? json_decode( $response['body'], true ) : array();
		}

		if ( ! is_array( $addons ) ) {
			$addons = array();
		}

		$addons['response_code'] = wp_remote_retrieve_response_code( $response );

		foreach ( $addons as $k => $addon ) {
			if ( ! is_array( $addon ) ) {
				continue;
			}

			if ( isset( $addon['categories'] ) ) {
				$cats = array_intersect( $this->skip_categories(), $addon['categories'] );

				if ( $cats ) {
					unset( $addons[ $k ] );
					continue;
				}
			}

			if ( ! array_key_exists( 'is_new', $addon ) && array_key_exists( 'released', $addon ) ) {
				$addons[ $k ]['is_new'] = $this->is_new( $addon );
			}
		}

		$this->set_cached( $addons );
		$this->done_running();

		return $addons;
	}

	/**
	 * Prevent multiple requests from running at the same time.
	 *
	 * @since 6.8.3
	 *
	 * @return bool
	 */
	protected function is_running() {
		if ( $this->run_as_multisite() ) {
			return get_site_transient( $this->transient_key() );
		}
		return get_transient( $this->transient_key() );
	}

	/**
	 * @since 6.8.3
	 *
	 * @return void
	 */
	protected function set_running() {
		$expires = 2 * MINUTE_IN_SECONDS;

		if ( $this->run_as_multisite() ) {
			set_site_transient( $this->transient_key(), true, $expires );
			return;
		}

		set_transient( $this->transient_key(), true, $expires );
	}

	/**
	 * @since 6.8.3
	 *
	 * @return void
	 */
	protected function done_running() {
		if ( $this->run_as_multisite() ) {
			delete_site_transient( $this->transient_key() );
		}
		delete_transient( $this->transient_key() );
	}

	/**
	 * Only allow one site in the network to make the api request at a time.
	 * If there is a license for the request, run individually.
	 *
	 * @since 6.8.3
	 *
	 * @return bool
	 */
	protected function run_as_multisite() {
		return is_multisite() && ! $this->license;
	}

	/**
	 * @since 6.8.3
	 *
	 * @return string
	 */
	protected function transient_key() {
		return strtolower( self::class ) . '_request_lock';
	}

	/**
	 * @since 3.06
	 *
	 * @return string
	 */
	protected function api_url() {
		if ( ! $this->license ) {
			// Direct traffic to Cloudflare worker when there is no license.
			return 'https://plapi.formidableforms.com/list/';
		}
		return 'https://formidableforms.com/wp-json/s11edd/v1/updates/';
	}

	/**
	 * @since 3.06
	 *
	 * @return string[]
	 */
	protected function skip_categories() {
		return array( 'WordPress Form Templates', 'WordPress Form Style Templates' );
	}

	/**
	 * @since 3.06
	 *
	 * @param object $license_plugin The FrmAddon object.
	 * @param array  $addons
	 *
	 * @return array
	 */
	public function get_addon_for_license( $license_plugin, $addons = array() ) {
		if ( ! $addons ) {
			$addons = $this->get_api_info();
		}

		$download_id = $license_plugin->download_id;
		$plugin      = array();

		if ( ! $download_id && $addons ) {
			foreach ( $addons as $addon ) {
				if ( is_array( $addon ) && ! empty( $addon['title'] ) && 0 === strcasecmp( $license_plugin->plugin_name, $addon['title'] ) ) {
					return $addon;
				}
			}
		} elseif ( isset( $addons[ $download_id ] ) ) {
			$plugin = $addons[ $download_id ];
		}

		return $plugin;
	}

	/**
	 * @since 3.06
	 *
	 * @return false|object
	 */
	public function get_pro_updater() {
		if ( FrmAppHelper::pro_is_installed() && is_callable( 'FrmProAppHelper::get_updater' ) ) {
			$updater = FrmProAppHelper::get_updater();
			$this->set_license( $updater->license );

			return $updater;
		}

		return false;
	}

	/**
	 * @since 3.06
	 *
	 * @return array|bool
	 */
	protected function get_cached() {
		$cache = $this->get_cached_option();

		if ( ! $cache ) {
			return false;
		}

		$is_expired = empty( $cache['timeout'] ) || time() > $cache['timeout'];

		if ( ! $is_expired && isset( $cache['version'] ) && $cache['version'] !== FrmAppHelper::plugin_version() ) {
			$is_expired = true;
		}

		// Avoid old cached data, unless we're currently trying to query for new data.
		// The call to $this->is_running likely triggers a database query, so only call if if we're expired.
		// (Rather than the other way around, which is less efficient).
		if ( $is_expired && ! $this->is_running() ) {
			return false;
		}

		return json_decode( $cache['value'], true );
	}

	/**
	 * Get the cache for the network if multisite.
	 *
	 * @since 6.8.3
	 *
	 * @return mixed
	 */
	protected function get_cached_option() {
		if ( is_multisite() ) {
			$cached = get_site_option( $this->cache_key );

			if ( $cached ) {
				return $cached;
			}
		}

		return get_option( $this->cache_key );
	}

	/**
	 * @since 3.06
	 *
	 * @param array $addons
	 *
	 * @return void
	 */
	protected function set_cached( $addons ) {
		$addons = $this->reduce_addon_data_before_caching( $addons );

		$data = array(
			'timeout' => strtotime( $this->get_cache_timeout( $addons ), time() ),
			'value'   => wp_json_encode( $addons ),
			'version' => FrmAppHelper::plugin_version(),
		);

		if ( is_multisite() ) {
			update_site_option( $this->cache_key, $data );
		} else {
			// Autoload the license cache because it gets called everywhere.
			$autoload = str_starts_with( $this->cache_key, 'frm_addons_l' );
			update_option( $this->cache_key, $data, $autoload );
		}
	}

	/**
	 * Remove certain add-on API data that we don't need to cache.
	 * This is to help keep the option data (which is auto-loaded) small.
	 *
	 * @since 6.28
	 *
	 * @param array $addons
	 *
	 * @return array
	 */
	private function reduce_addon_data_before_caching( $addons ) {
		if ( is_subclass_of( $this, 'FrmFormApi' ) ) {
			// We only want to modify FrmFormApi. Leave the other APIs alone for now.
			return $addons;
		}

		$reduced_addons = array();

		foreach ( $addons as $key => $addon ) {
			if ( ! is_array( $addon ) ) {
				$reduced_addons[ $key ] = $addon;
				continue;
			}

			if ( ! $this->should_include_addon_in_cached_data( $addon ) ) {
				continue;
			}

			if ( isset( $addon['changelog'] ) ) {
				unset( $addon['changelog'], $addon['banners'] );
			}

			$reduced_addons[ $key ] = $addon;
		}

		return $reduced_addons;
	}

	/**
	 * @since 6.28
	 *
	 * @param array $addon
	 *
	 * @return bool True if the add-on should be included in cached data.
	 */
	private function should_include_addon_in_cached_data( $addon ) {
		if ( isset( $addon['version'] ) && '' === $addon['version'] ) {
			// If version is set but blank, the plugin is not actually live.
			return false;
		}

		if ( isset( $addon['categories'] ) ) {
			if ( isset( $addon['slug'] ) && 'views' === $addon['slug'] ) {
				// Legacy views has no categories set, but we should still
				// Include it in cache since it is a valid add-on.
				return true;
			}

			$categories_are_empty = ! $addon['categories'] || $addon['categories'] === array( 'Strategy11' );

			if ( $categories_are_empty ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * If the last check was a a rate limit, we'll need to check again sooner.
	 *
	 * @since 6.8.3
	 *
	 * @param array $addons
	 *
	 * @return string
	 */
	protected function get_cache_timeout( $addons ) {
		if ( isset( $addons['response_code'] ) && 429 === $addons['response_code'] ) {
			return '+5 minutes';
		}
		return $this->cache_timeout;
	}

	/**
	 * @since 3.06
	 *
	 * @return void
	 */
	public function reset_cached() {
		if ( is_multisite() ) {
			delete_site_option( $this->cache_key );
		} else {
			delete_option( $this->cache_key );
		}
		$this->done_running();
	}

	/**
	 * @since 3.06
	 *
	 * @return array
	 */
	public function error_for_license() {
		return $this->license ? $this->get_error_from_response() : array();
	}

	/**
	 * @since 3.06
	 *
	 * @param array $addons
	 *
	 * @return array
	 */
	public function get_error_from_response( $addons = array() ) {
		if ( ! $addons ) {
			$addons = $this->get_api_info();
		}

		$errors = array();

		if ( ! isset( $addons['error'] ) ) {
			return $errors;
		}

		if ( is_string( $addons['error'] ) ) {
			$errors[] = $addons['error'];
		} elseif ( ! empty( $addons['error']['message'] ) ) {
			$errors[] = $addons['error']['message'];
		}

		do_action( 'frm_license_error', $addons['error'] );

		return $errors;
	}

	/**
	 * Check if a template is new.
	 *
	 * @since 6.0
	 *
	 * @param array $addon
	 *
	 * @return bool
	 */
	protected function is_new( $addon ) {
		return strtotime( $addon['released'] ) > strtotime( '-' . $this->new_days . ' days' );
	}
}
