<?php
/**
 * Connects the feature gate to Freemius, the merchant of record.
 *
 * Freemius handles checkout (card and PayPal), license keys, renewals, EU VAT
 * and the customer portal where buyers retrieve receipts and keys. This class
 * is the whole of DataStream's side of that: it boots the SDK when present and
 * translates its answers into the two filters the gate already reads.
 *
 * Deliberate design points, all of them paid for on SwiftQueue first:
 *
 *  - Nothing is gated until the store is actually configured. If the SDK is
 *    missing or the credentials are blank, is_configured() is false,
 *    enforcement stays off, and every feature keeps working. An unconfigured
 *    install must never degrade a site that was working, and a failed Freemius
 *    API call must never lock a paying customer out of what they bought.
 *
 *  - The SDK is optional. DataStream is GPL and its core is free forever; it
 *    has to run for someone who deletes vendor/freemius entirely.
 *
 *  - is_premium is derived, never hardcoded. See is_premium_build().
 *
 * @package DataStream
 */

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

/**
 * Class DataStream_License
 */
class DataStream_License {

	/**
	 * Freemius product id, public key and slug.
	 *
	 * These are not secrets -- the public key is designed to ship in client
	 * code -- but they are per-store, so they are read from constants or the
	 * config file rather than hardcoded here.
	 */
	private const CONFIG_CONSTANTS = array(
		'id'         => 'DATASTREAM_FS_ID',
		'public_key' => 'DATASTREAM_FS_PUBLIC_KEY',
		'slug'       => 'DATASTREAM_FS_SLUG',
	);

	/**
	 * Freemius instance, once booted.
	 *
	 * @var object|null
	 */
	private static $freemius = null;

	/**
	 * Reads store configuration, preferring constants over the config file.
	 *
	 * @return array<string, string>
	 */
	public static function get_config() {
		$config = array();
		$file   = DATASTREAM_PLUGIN_DIR . 'includes/freemius-config.php';

		if ( is_readable( $file ) ) {
			$from_file = include $file;

			if ( is_array( $from_file ) ) {
				$config = $from_file;
			}
		}

		foreach ( self::CONFIG_CONSTANTS as $key => $constant ) {
			if ( defined( $constant ) && '' !== (string) constant( $constant ) ) {
				$config[ $key ] = (string) constant( $constant );
			}
		}

		return array(
			'id'         => (string) ( $config['id'] ?? '' ),
			'public_key' => (string) ( $config['public_key'] ?? '' ),
			'slug'       => (string) ( $config['slug'] ?? 'datastream-api-sync' ),
		);
	}

	/**
	 * Whether this install has a real store behind it.
	 *
	 * Both the product id and the public key must be present. A half-filled
	 * config is treated as no config at all -- booting the SDK with a bogus id
	 * produces confusing API errors in the admin, which is worse than simply
	 * behaving like the unlicensed-but-unrestricted build.
	 *
	 * @return bool
	 */
	public static function is_configured() {
		$config = self::get_config();

		return '' !== $config['id']
			&& '' !== $config['public_key']
			&& self::sdk_is_available();
	}

	/**
	 * Whether the vendored SDK is present and loadable.
	 *
	 * @return bool
	 */
	public static function sdk_is_available() {
		return is_readable( DATASTREAM_PLUGIN_DIR . 'vendor/freemius/start.php' )
			&& self::wordpress_is_loaded();
	}

	/**
	 * Whether enough of WordPress exists for the SDK to boot.
	 *
	 * The SDK reaches for core functions at include time -- get_theme_root()
	 * runs before any hook -- so requiring it outside a real WordPress request
	 * is an immediate fatal. In production that never happens, because
	 * initialize() runs on plugins_loaded. It does happen in the test harness,
	 * which stubs enough of WordPress to exercise the plugin but not the whole
	 * theme subsystem. Checking a function the SDK actually calls is more
	 * honest than checking ABSPATH, which any bootstrap can define.
	 *
	 * @return bool
	 */
	private static function wordpress_is_loaded() {
		return function_exists( 'get_theme_root' ) && function_exists( 'get_stylesheet' );
	}

	/**
	 * Boots Freemius and wires the gate. Safe to call when nothing is set up.
	 */
	public static function initialize() {
		if ( ! self::is_configured() ) {
			// No store yet: leave every feature unlocked and say nothing.
			return;
		}

		if ( null === self::boot_sdk() ) {
			return;
		}

		add_filter( 'datastream_is_pro', array( __CLASS__, 'filter_is_pro' ) );
		add_filter( 'datastream_enforce_license', '__return_true' );
	}

	/**
	 * Boots the Freemius SDK once.
	 *
	 * @return object|null
	 */
	private static function boot_sdk() {
		if ( null !== self::$freemius ) {
			return self::$freemius;
		}

		$config = self::get_config();

		if ( ! function_exists( 'fs_dynamic_init' ) ) {
			require_once DATASTREAM_PLUGIN_DIR . 'vendor/freemius/start.php';
		}

		if ( ! function_exists( 'fs_dynamic_init' ) ) {
			return null;
		}

		self::$freemius = fs_dynamic_init(
			array(
				'id'                  => $config['id'],
				'slug'                => $config['slug'],
				'type'                => 'plugin',
				'public_key'          => $config['public_key'],
				'is_premium'          => self::is_premium_build(),
				// The core sync engine is GPL and free forever, so the free
				// tier is genuinely usable rather than a crippled trial.
				'has_premium_version' => true,
				'has_addons'          => false,
				'has_paid_plans'      => true,
				// Required for the wp.org-hosted free build: it makes the SDK
				// show the skippable opt-in instead of a license wall.
				'is_org_compliant'    => true,
				'menu'                => array(
					'slug'    => 'datastream',
					'account' => true,
					'contact' => false,
					'support' => false,
				),
			)
		);

		return self::$freemius;
	}

	/**
	 * Translates Freemius license state into the gate's answer.
	 *
	 * is_paying_or_trial() rather than is_paying(): a trial user is entitled
	 * to the paid features for the length of the trial.
	 *
	 * @param bool $is_pro Current answer from the gate.
	 * @return bool
	 */
	public static function filter_is_pro( $is_pro ) {
		if ( $is_pro ) {
			return true;
		}

		$instance = self::$freemius;

		if ( ! is_object( $instance ) || ! method_exists( $instance, 'is_paying_or_trial' ) ) {
			return false;
		}

		return (bool) $instance->is_paying_or_trial();
	}

	/**
	 * The booted Freemius instance, or null when no store is running.
	 *
	 * @return object|null
	 */
	public static function get_instance() {
		return self::$freemius;
	}

	/**
	 * Whether this copy of the plugin is the premium build.
	 *
	 * Decided by looking at the filesystem rather than by a hardcoded flag. The
	 * free build is generated by DELETING the files listed under
	 * fs_premium_only in the main plugin file; checking for one of them tells
	 * this copy which build it is. is_premium matters because it selects the
	 * SDK's first-run screen: true shows a license-key wall, false shows the
	 * skippable opt-in -- and WordPress.org will only accept the latter.
	 *
	 * A hardcoded true here is exactly how a license wall once ended up in
	 * front of every SwiftQueue user, free ones included. Deriving it also
	 * means Freemius' deployment preprocessor -- which rewrites a literal
	 * is_premium => true when it finds one -- has nothing to rewrite: both
	 * builds answer correctly at runtime whatever the preprocessor did.
	 *
	 * The marker file must stay on the premium-only list;
	 * tools/verify-free-build.sh walks that same list.
	 *
	 * @return bool
	 */
	public static function is_premium_build() {
		return is_readable( DATASTREAM_PLUGIN_DIR . 'includes/class-datastream-post-mapper.php' );
	}

	/**
	 * Checkout URL, used by upgrade CTAs in the admin.
	 *
	 * Falls back to the public pricing page when no store is configured, so an
	 * upgrade button never dead-ends.
	 *
	 * @param string $plan Optional plan slug.
	 * @return string
	 */
	public static function get_checkout_url( $plan = '' ) {
		if ( ! self::is_configured() ) {
			return 'https://utahcreates.com/datastream/';
		}

		$instance = self::$freemius;

		if ( is_object( $instance ) && method_exists( $instance, 'get_upgrade_url' ) ) {
			return (string) $instance->get_upgrade_url();
		}

		$config = self::get_config();
		$url    = sprintf( 'https://checkout.freemius.com/plugin/%s/', rawurlencode( $config['id'] ) );

		return '' === $plan ? $url : $url . 'plan/' . rawurlencode( $plan ) . '/';
	}

	/**
	 * Where buyers retrieve receipts, invoices and license keys.
	 *
	 * @return string
	 */
	public static function get_customer_portal_url() {
		return 'https://users.freemius.com/';
	}
}
