<?php
/**
 * This is the class that handles the overall logic for design assets.
 *
 * @since   2.0.0
 * @license GPL-2.0-or-later
 * @package Style Manager
 */

declare ( strict_types=1 );

namespace Pixelgrade\StyleManager\Customize;

use Pixelgrade\StyleManager\Client\CloudInterface;
use Pixelgrade\StyleManager\Provider\Options;
use Pixelgrade\StyleManager\Vendor\Cedaro\WP\Plugin\AbstractHookProvider;
use Pixelgrade\StyleManager\Vendor\Psr\Log\LoggerInterface;

/**
 * Provides the design assets logic.
 *
 * @since 2.0.0
 */
class DesignAssets extends AbstractHookProvider {

	const CACHE_KEY = 'style_manager_design_assets';
	const CACHE_TIMESTAMP_KEY = 'style_manager_design_assets_timestamp';

	/**
	 * The current design assets config.
	 * @var     array|null
	 */
	protected ?array $design_assets = null;

	/**
	 * Cloud client.
	 *
	 * @var CloudInterface
	 */
	protected CloudInterface $cloud_client;

	/**
	 * Logger.
	 *
	 * @var LoggerInterface
	 */
	protected LoggerInterface $logger;

	/**
	 * Constructor.
	 *
	 * @since 2.0.0
	 *
	 * @param CloudInterface  $cloud_client Cloud client.
	 * @param LoggerInterface $logger       Logger.
	 */
	public function __construct(
		CloudInterface $cloud_client,
		LoggerInterface $logger
	) {
		$this->cloud_client = $cloud_client;
		$this->logger = $logger;
	}

	/**
	 * Register hooks.
	 *
	 * @since 2.0.0
	 */
	public function register_hooks() {
		// Handle various cache invalidations, in a proactive manner.
		$this->add_action( 'style_manager/invalidate_all_caches', 'invalidate_cache', 1 );
	}

	/**
	 * Get the entire design assets data.
	 *
	 * @since 2.0.0
	 *
	 * @param bool $skip_cache Optional. Whether to use the cached data or fetch new data.
	 *
	 * @return array
	 */
	public function get( bool $skip_cache = false ): array {
		if ( ! is_null( $this->design_assets ) && false === $skip_cache ) {
			return $this->design_assets;
		}

		$design_assets = $this->maybe_fetch( $skip_cache );
		if ( ! is_array( $design_assets ) ) {
			$design_assets = [];
		}
		$this->design_assets = $design_assets;

		// Determine if we should use the config in the theme root and skip the external config entirely.
		if ( defined('STYLE_MANAGER_LOAD_THEME_ROOT_CONFIG') && true === STYLE_MANAGER_LOAD_THEME_ROOT_CONFIG ) {
			$this->design_assets = $this->maybe_load_theme_config_from_theme_root( $this->design_assets );
		}

		$this->design_assets = apply_filters( 'style_manager/get_design_assets', $this->design_assets );
		if ( ! is_array( $this->design_assets ) ) {
			$this->design_assets = [];
		}

		return $this->design_assets;
	}

	/**
	 * Get a certain design assets entry data.
	 *
	 * @since 2.0.0
	 *
	 * @param string $entry      The entry to return the design asset data.
	 * @param bool   $skip_cache Optional. Whether to use the cached config or fetch a new one.
	 *
	 * @return array|null The entry data. If the entry is not found, null will be returned.
	 */
	public function get_entry( string $entry, bool $skip_cache = false ): ?array {
		$this->get( $skip_cache );

		if ( isset( $this->design_assets[ $entry ] ) ) {
			return $this->design_assets[ $entry ];
		}

		return null;
	}

	/**
	 * Fetch the design assets data from the Pixelgrade Cloud.
	 *
	 * Caches the data for 6 hours. Use local defaults if not available.
	 *
	 * @since 2.0.0
	 *
	 * @param bool $skip_cache Optional. Whether to use the cached data or fetch a new one.
	 *
	 * @return array|false
	 */
	protected function maybe_fetch( bool $skip_cache = false ) {
		// First try and get the cached data.
		$data = get_option( self::CACHE_KEY );

		// For performance reasons, we will ONLY fetch remotely when in the WP ADMIN area or via an ADMIN AJAX call, regardless of settings.
		if ( ! is_admin() ) {
			return $data;
		}

		// We don't force skip the cache for AJAX requests for performance reasons.
		if ( ! wp_doing_ajax() && defined('STYLE_MANAGER_ALWAYS_FETCH_DESIGN_ASSETS' ) && true === STYLE_MANAGER_ALWAYS_FETCH_DESIGN_ASSETS ) {
			$skip_cache = true;
		}

		$expire_timestamp = false;

		// Only try to get the expire timestamp if we really need to.
		if ( true !== $skip_cache && false !== $data ) {
			// Get the cache data expiration timestamp.
			$expire_timestamp = get_option( self::CACHE_TIMESTAMP_KEY );
		}

		// The data isn't set, is expired, or we were instructed to skip the cache; we need to fetch fresh data.
		if ( true === $skip_cache || false === $data || false === $expire_timestamp || $expire_timestamp < time() ) {
			$previous_data = $data;

			// Fetch the design assets from the cloud.
			$fetched_data = $this->cloud_client->fetch_design_assets();
			// Bail in case of failure to retrieve data.
			// We will return the data already available.
			if ( is_null( $fetched_data ) ) {
				// Cache the failure with a short TTL so we don't hammer the cloud on every admin
				// request when it's unreachable (dev installs, network errors, cloud downtime).
				//
				// We need to seed CACHE_KEY with an empty array (not leave it as false), because
				// the re-fetch gate below is `false === $data || false === $expire_timestamp ||
				// $expire_timestamp < time()` — a false $data forces a re-fetch regardless of the
				// timestamp. Setting it to [] satisfies the gate; consumers tolerate [] (it flows
				// through `if ( ! is_array( $design_assets ) ) { $design_assets = []; }` as-is).
				// See pixelgrade/style-manager#87.
				if ( false === $data ) {
					update_option( self::CACHE_KEY, [], false );
				}
				update_option( self::CACHE_TIMESTAMP_KEY, time() + 5 * MINUTE_IN_SECONDS, false );
				return $data;
			}

			// Preserve the last known good cloud font palettes when the cloud retries
			// succeed without that entry. This avoids collapsing back to hard-coded
			// defaults when the cloud font palettes endpoint is temporarily broken.
			if ( is_array( $data ) && isset( $data['font_palettes'] ) && ! isset( $fetched_data['font_palettes'] ) ) {
				$fetched_data['font_palettes'] = $data['font_palettes'];
			}

			$data = $fetched_data;

			// Cache the data in an option for 6 hours.
			// Not autoloaded — only consumed in admin/Customizer paths (see is_admin() bail above);
			// keeping it out of the autoload payload saves ~117 KB per WP request. See pixelgrade/style-manager#86.
			update_option( self::CACHE_KEY, $data, false );
			update_option( self::CACHE_TIMESTAMP_KEY, time() + 6 * HOUR_IN_SECONDS, false );

			if ( $previous_data !== $data ) {
				static::invalidate_options_caches();
			}
		}

		return apply_filters( 'style_manager/maybe_fetch_design_assets', $data );
	}

	protected static function invalidate_cache() {
		update_option( self::CACHE_TIMESTAMP_KEY , time() - 24 * HOUR_IN_SECONDS, false );
	}

	/**
	 * Invalidate dependent Options caches after design assets change.
	 *
	 * Design assets feed the generated Customizer config and option details caches. If
	 * those caches stay warm after a cloud refresh, the UI can keep rendering stale
	 * palettes until the 24h Options TTL expires.
	 *
	 * @since 2.0.0
	 */
	protected static function invalidate_options_caches(): void {
		update_option( Options::CUSTOMIZER_CONFIG_CACHE_TIMESTAMP_KEY, time() - 24 * HOUR_IN_SECONDS, false );
		update_option( Options::DETAILS_CACHE_TIMESTAMP_KEY, time() - 24 * HOUR_IN_SECONDS, false );
	}

	/**
	 * Include the "external" config file in the theme root and overwrite the existing theme configs.
	 *
	 * @since 2.0.0
	 *
	 * @param array $design_assets
	 *
	 * @return array
	 */
	protected function maybe_load_theme_config_from_theme_root( array $design_assets ): array {
		$file_name = 'sm_theme_root.php';

		// First gather details about the current (parent) theme.
		$theme = wp_get_theme( get_template() );
		// Bail if for some strange reason we couldn't find the theme.
		if ( ! $theme->exists() ) {
			return $design_assets;
		}

		$file = trailingslashit( $theme->get_template_directory() ) . $file_name;
		if ( ! file_exists( $file ) ) {
			return $design_assets;
		}

		// We expect to get from the file include a $config variable with the entire Style Manager (partial) config.
		include $file;

		if ( ! isset( $config ) || ! is_array( $config ) || empty( $config['sections'] ) ) {
			// Alert the developers that things are not alright.
			_doing_it_wrong( __METHOD__, 'The Style Manager theme root config is not good - the `sections` entry is missing. Please check it! For now, we will ignore it.', null );

			return $design_assets;
		}

		// Construct the pseudo-external theme config.

		// Start with a clean slate.
		$design_assets['theme_configs'] = [];

		$design_assets['theme_configs']['theme_root'] = [
			'id'            => 1,
			'name'          => $theme->get( 'Name' ),
			'slug'          => $theme->get_stylesheet(),
			'txtd'          => $theme->get( 'TextDomain' ),
			'loose_match'   => true,
			'config'        => $config,
			'created'       => gmdate('Y-m-d H:i:s'),
			'last_modified' => gmdate('Y-m-d H:i:s'),
			'hashid'        => 'theme_root',
		];

		return $design_assets;
	}
}
