<?php

namespace NewfoldLabs\WP\Module\Performance\Cache\Types;

use NewfoldLabs\WP\Module\Performance\Cache\Purgeable;
use NewfoldLabs\WP\Module\Performance\OptionListener;
use NewfoldLabs\WP\ModuleLoader\Container;
use NewfoldLabs\WP\Module\Performance\Cache\CacheExclusion;
use NewfoldLabs\WP\Module\Performance\Cache\CacheManager;
use NewfoldLabs\WP\Module\Performance\Cache\Types\Fragments\FileCacheFragment;
use NewfoldLabs\WP\Module\Htaccess\Api as HtaccessApi;
use wpscholar\Url;

use function NewfoldLabs\WP\Module\Performance\get_cache_level;
use function NewfoldLabs\WP\Module\Performance\remove_directory;
use function NewfoldLabs\WP\Module\Performance\should_cache_pages;
use function NewfoldLabs\WP\ModuleLoader\container as getContainer;

/**
 * File cache type.
 */
class File extends CacheBase implements Purgeable {
	/**
	 * The directory where cached files live.
	 *
	 * @var string
	 */
	const CACHE_DIR = WP_CONTENT_DIR . '/newfold-page-cache/';

	/**
	 * Human-friendly marker label used in BEGIN/END comments.
	 *
	 * @var string
	 */
	const MARKER = 'Newfold File Cache';

	/**
	 * Registry identifier for this fragment.
	 * Must be globally unique across fragments.
	 *
	 * @var string
	 */
	const FRAGMENT_ID = 'nfd.cache.file';

	/**
	 * Whether or not the code for this cache type should be loaded.
	 *
	 * @param Container $container Dependency injection container.
	 * @return bool
	 */
	public static function should_enable( Container $container ) {
		return (bool) $container->has( 'isApache' ) && $container->get( 'isApache' );
	}

	/**
	 * Constructor.
	 */
	public function __construct() {
		new OptionListener( CacheManager::OPTION_CACHE_LEVEL, array( __CLASS__, 'maybeAddRules' ) );
		new OptionListener( CacheExclusion::OPTION_CACHE_EXCLUSION, array( __CLASS__, 'exclusionChange' ) );

		add_action( 'init', array( $this, 'maybeGeneratePageCache' ) );
		add_action( 'newfold_update_htaccess', array( $this, 'on_rewrite' ) );
	}

	/**
	 * Manage on exclusion option change.
	 *
	 * @return void
	 */
	public static function exclusionChange() {
		self::maybeAddRules( get_cache_level() );
	}

	/**
	 * When updating mod rewrite rules, also update our rewrites as appropriate.
	 *
	 * @return void
	 */
	public function on_rewrite() {
		self::maybeAddRules( get_cache_level() );
	}

	/**
	 * Determine whether to add or remove rules based on caching level and brand.
	 *
	 * @param int $cache_level The caching level.
	 * @return void
	 */
	public static function maybeAddRules( $cache_level ) {
		$brand = getContainer()->plugin()->brand;

		if ( absint( $cache_level ) > 1 && 'bluehost' !== $brand && 'hostgator' !== $brand ) {
			self::addRules();
		} else {
			self::removeRules();
		}
	}

	/**
	 * Register (or replace) our fragment with current settings.
	 *
	 * @return void
	 */
	public static function addRules() {
		// Compute base path and relative cache directory path for rewrite rules.
		$base_path      = (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH );
		$rel_cache_path = str_replace( trailingslashit( get_home_path() ), '/', trailingslashit( self::CACHE_DIR ) );

		// Build optional exclusion pattern (pipe-separated).
		$exclusion_pattern = '';
		$cache_exclusion   = get_option( CacheExclusion::OPTION_CACHE_EXCLUSION, '' );

		if ( is_string( $cache_exclusion ) && '' !== $cache_exclusion ) {
			$parts             = array_map( 'trim', explode( ',', sanitize_text_field( $cache_exclusion ) ) );
			$exclusion_pattern = implode( '|', array_filter( $parts ) );
		}

		HtaccessApi::register(
			new FileCacheFragment(
				self::FRAGMENT_ID,
				self::MARKER,
				$base_path,
				$rel_cache_path,
				$exclusion_pattern
			),
			true // queue apply
		);
	}

	/**
	 * Unregister our fragment.
	 *
	 * @return void
	 */
	public static function removeRules() {
		HtaccessApi::unregister( self::FRAGMENT_ID );
	}

	/**
	 * Initiate the generation of a page cache for a given request, if necessary.
	 *
	 * @return void
	 */
	public function maybeGeneratePageCache() {
		if ( $this->isCacheable() ) {
			if ( $this->shouldCache() ) {
				ob_start( array( $this, 'write' ) );
			}
		} else {
			nocache_headers();
		}
	}

	/**
	 * Write page content to cache.
	 *
	 * @param  string $content  Page content to be cached.
	 * @return string
	 */
	public function write( $content ) {
		if ( ! empty( $content ) ) {

			$path = $this->getStoragePathForRequest();
			$file = $this->getStorageFileForRequest();

			if ( false !== strpos( $content, '</html>' ) ) {
				$content .= "\n<!--Generated by Newfold Page Cache-->";
			}

			global $wp_filesystem;

			if ( ! function_exists( 'WP_Filesystem' ) ) {
				require_once ABSPATH . 'wp-admin/includes/file.php';
			}

			WP_Filesystem();

			if ( ! $wp_filesystem->is_dir( $path ) ) {
				$wp_filesystem->mkdir( $path, 0755 );
			}

			$wp_filesystem->put_contents( $file, $content, FS_CHMOD_FILE );
		}

		return $content;
	}

	/**
	 * Check if the current request is cacheable.
	 *
	 * @return bool
	 */
	public function isCacheable() {
		// The request URI should never be empty – even for the homepage it should be '/'
		if ( empty( $_SERVER['REQUEST_URI'] ) ) {
			return false;
		}

		// Don't cache if pretty permalinks are disabled
		if ( false === get_option( 'permalink_structure' ) ) {
			return false;
		}

		// Only cache front-end pages
		if ( is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
			return false;
		}

		// Don't cache REST API requests
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return false;
		}

		// Never cache requests made via WP-CLI
		if ( defined( 'WP_CLI' ) && WP_CLI ) {
			return false;
		}

		// Don't cache if there are URL parameters present
		if ( isset( $_GET ) && ! empty( $_GET ) ) { // phpcs:ignore WordPress.Security.NonceVerification
			return false;
		}

		// Don't cache if handling a form submission
		if ( isset( $_POST ) && ! empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification
			return false;
		}

		// Don't cache if a user is logged in.
		if ( function_exists( 'is_user_logged_in' ) && is_user_logged_in() ) {
			return false;
		}

		global $wp_query;
		if ( isset( $wp_query ) ) {

			// Don't cache 404 pages or RSS feeds
			if ( is_404() || is_feed() ) {
				return false;
			}
		}

		// Don't cache private pages
		if ( 'private' === get_post_status() ) {
			return false;
		}

		return true;
	}

	/**
	 * Check if we should cache the current request.
	 *
	 * @return bool
	 */
	public function shouldCache() {
		// If page caching is disabled, then don't cache
		if ( ! should_cache_pages() ) {
			return false;
		}

		// Check cache exclusion (application-level).
		$cache_exclusion_parameters = $this->exclusions();
		if ( ! empty( $cache_exclusion_parameters ) ) {
			foreach ( $cache_exclusion_parameters as $param ) {
				if ( stripos( $_SERVER['REQUEST_URI'], $param ) !== false ) {
					return false;
				}
			}
		}

		// Don't cache if a file exists and hasn't expired.
		$file = $this->getStorageFileForRequest();
		if ( file_exists( $file ) && filemtime( $file ) + $this->getExpirationTimeframe() > time() ) {
			return false;
		}

		return true;
	}

	/**
	 * Get an array of strings that should not be present in the URL for a request to be cached.
	 *
	 * @return array
	 */
	protected function exclusions() {
		$default                = array( 'cart', 'checkout', 'wp-admin', '@', '%', ':', ';', '&', '=', '.', rest_get_url_prefix() );
		$cache_exclusion_option = array_map( 'trim', explode( ',', (string) get_option( CacheExclusion::OPTION_CACHE_EXCLUSION ) ) );
		return array_merge( $default, array_filter( $cache_exclusion_option ) );
	}

	/**
	 * Get expiration duration.
	 *
	 * @return int
	 */
	protected function getExpirationTimeframe() {
		switch ( get_cache_level() ) {
			case 2:
				return 2 * HOUR_IN_SECONDS;
			case 3:
				return 8 * HOUR_IN_SECONDS;
			default:
				return 0;
		}
	}

	/**
	 * Purge everything from the cache.
	 *
	 * @return void
	 */
	public function purge_all() {
		remove_directory( self::CACHE_DIR );
	}

	/**
	 * Purge a specific URL from the cache.
	 *
	 * @param string $url the url to purge.
	 * @return void
	 */
	public function purge_url( $url ) {
		$path = $this->getStoragePathForRequest();

		if ( trailingslashit( self::CACHE_DIR ) === $path ) {
			if ( file_exists( self::CACHE_DIR . '/_index.html' ) ) {
				wp_delete_file( self::CACHE_DIR . '/_index.html' );
			}

			return;
		}

		remove_directory( $this->getStoragePathForRequest() );
	}

	/**
	 * Get storage path for a given request.
	 *
	 * @return string
	 */
	protected function getStoragePathForRequest() {
		static $path;

		if ( ! isset( $path ) ) {
			$url       = new Url();
			$base_path = (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH );
			$path      = trailingslashit( self::CACHE_DIR . str_replace( $base_path, '', esc_url( $url->path ) ) );
		}

		return $path;
	}

	/**
	 * Get storage file for a given request.
	 *
	 * @return string
	 */
	protected function getStorageFileForRequest() {
		return $this->getStoragePathForRequest() . '_index.html';
	}

	/**
	 * Handle activation logic.
	 *
	 * @return void
	 */
	public static function on_activation() {
		self::maybeAddRules( get_cache_level() );
	}

	/**
	 * Handle deactivation logic.
	 *
	 * @return void
	 */
	public static function on_deactivation() {
		// Remove file cache rules from .htaccess via fragment unregister.
		self::removeRules();

		// Remove all statically cached files.
		remove_directory( self::CACHE_DIR );
	}
}
