<?php
declare(strict_types=1);

namespace WP_Rocket\Addon\Cloudflare;

use WP_Post;
use WP_Rocket\Event_Management\Subscriber_Interface;
use WP_Rocket\Admin\{Options, Options_Data};
use WPMedia\Cloudflare\Auth\AuthFactoryInterface;

class Subscriber implements Subscriber_Interface {
	/**
	 * Cloudflare instance.
	 *
	 * @var Cloudflare
	 */
	private $cloudflare;

	/**
	 * Options Data instance.
	 *
	 * @var Options_Data
	 */
	private $options;

	/**
	 * Options instance.
	 *
	 * @var Options
	 */
	private $options_api;

	/**
	 * Authentication factory.
	 *
	 * @var AuthFactoryInterface
	 */
	protected $auth_factory;

	/**
	 * Creates an instance of the Cloudflare Subscriber.
	 *
	 * @param Cloudflare           $cloudflare Cloudflare instance.
	 * @param Options_Data         $options WP Rocket options instance.
	 * @param Options              $options_api Options instance.
	 * @param AuthFactoryInterface $auth_factory Authentication factory.
	 */
	public function __construct( Cloudflare $cloudflare, Options_Data $options, Options $options_api, AuthFactoryInterface $auth_factory ) {
		$this->options      = $options;
		$this->options_api  = $options_api;
		$this->cloudflare   = $cloudflare;
		$this->auth_factory = $auth_factory;
	}

	/**
	 * Returns an array of events that this subscriber wants to listen to.
	 *
	 * @return array
	 */
	public static function get_subscribed_events() {
		$slug = rocket_get_constant( 'WP_ROCKET_SLUG', 'wp_rocket_settings' );

		return [
			'rocket_varnish_ip'                         => 'set_varnish_localhost',
			'rocket_varnish_purge_request_host'         => 'set_varnish_purge_request_host',
			'rocket_cron_deactivate_cloudflare_devmode' => 'deactivate_devmode',
			'rocket_after_clean_domain'                 => 'auto_purge',
			'after_rocket_clean_post'                   => [ 'auto_purge_by_url', 10, 3 ],
			'after_rocket_clean_home'                   => [ 'purge_url', 10, 2 ],
			'after_rocket_clean_files'                  => 'purge_url',
			'admin_post_rocket_purge_cloudflare'        => 'purge_cache',
			'init'                                      => [ 'set_real_ip', 1 ],
			'update_option_' . $slug                    => [
				[ 'save_cloudflare_options', 10, 2 ],
				[ 'update_dev_mode', 11, 2 ],
			],
			'pre_update_option_' . $slug                => [
				[ 'change_auth', 8, 2 ],
				[ 'delete_connection_transient', 10, 2 ],
				[ 'save_cloudflare_old_settings', 10, 2 ],
				[ 'display_settings_notice', 11, 2 ],
			],
			'rocket_buffer'                             => [ 'protocol_rewrite', PHP_INT_MAX ],
			'wp_calculate_image_srcset'                 => [ 'protocol_rewrite_srcset', PHP_INT_MAX ],
			'rocket_cdn_helper_addons'                  => 'add_cdn_helper_message',
		];
	}

	/**
	 * Sets the Varnish IP to localhost if Cloudflare is active.
	 *
	 * @param string|array $varnish_ip Varnish IP.
	 *
	 * @return array
	 */
	public function set_varnish_localhost( $varnish_ip ) {
		if ( ! $this->should_filter_varnish() ) {
			return $varnish_ip;
		}

		if ( is_string( $varnish_ip ) ) {
			$varnish_ip = (array) $varnish_ip;
		}

		$varnish_ip[] = 'localhost';

		return $varnish_ip;
	}

	/**
	 * Sets the Host header to the website domain if Cloudflare is active.
	 *
	 * @param string $host the host header value.
	 *
	 * @return string
	 */
	public function set_varnish_purge_request_host( $host ) {
		if ( ! $this->should_filter_varnish() ) {
			return $host;
		}

		return wp_parse_url( home_url(), PHP_URL_HOST );
	}

	/**
	 * Checks if we should filter the value for the Varnish purge.
	 *
	 * @return bool
	 */
	private function should_filter_varnish(): bool {
		// This filter is documented in inc/Addon/Varnish.php.
		return apply_filters( 'do_rocket_varnish_http_purge', false ) // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals
			||
			$this->options->get( 'varnish_auto_purge', 0 );
	}

	/**
	 * Automatically set Cloudflare development mode value to off after 3 hours to reflect Cloudflare behaviour.
	 *
	 * @return void
	 */
	public function deactivate_devmode() {
		$this->options->set( 'cloudflare_devmode', 0 );
		$this->options_api->set( 'settings', $this->options->get_options() );
	}

	/**
	 * Purge Cloudflare cache automatically if Cache Everything is set as a Page Rule.
	 *
	 * @return void
	 */
	public function auto_purge() {
		if ( ! current_user_can( 'rocket_purge_cloudflare_cache' ) ) {
			return;
		}

		$settings = $this->options_api->get( 'settings', [] );

		$this->options->set_values( $settings );

		$auth = $this->auth_factory->create( $settings );

		$this->cloudflare->change_auth( $auth );

		if ( is_wp_error( $this->cloudflare->check_connection( $this->options->get( 'cloudflare_zone_id', '' ) ) ) ) {
			return;
		}

		$cf_cache_everything = $this->cloudflare->has_page_rule( 'cache_everything' );

		if ( is_wp_error( $cf_cache_everything ) || ! $cf_cache_everything ) {
			return;
		}

		// Purge CloudFlare.
		$this->cloudflare->purge_cloudflare();
	}

	/**
	 * Purge Cloudflare cache URLs automatically if Cache Everything is set as a Page Rule.
	 *
	 * @param WP_Post $post       The post object.
	 * @param array   $purge_urls URLs cache files to remove.
	 * @param string  $lang       The post language.
	 *
	 * @return void
	 */
	public function auto_purge_by_url( $post, $purge_urls, $lang ) {
		if ( ! current_user_can( 'rocket_purge_cloudflare_cache' ) ) {
			return;
		}

		if ( is_wp_error( $this->cloudflare->check_connection() ) ) {
			return;
		}

		$cf_cache_everything = $this->cloudflare->has_page_rule( 'cache_everything' );

		if ( is_wp_error( $cf_cache_everything ) || ! $cf_cache_everything ) {
			return;
		}

		// Add home URL and feeds URLs to Cloudflare clean cache URLs list.
		$purge_urls[] = get_rocket_i18n_home_url( $lang );
		$feed_urls    = [];
		$feed_urls[]  = get_feed_link();
		$feed_urls[]  = get_feed_link( 'comments_' );

		// this filter is documented in inc/functions/files.php.
		$feed_urls  = apply_filters( 'rocket_clean_home_feeds', $feed_urls );
		$purge_urls = array_unique( array_merge( $purge_urls, $feed_urls ) );

		// Purge CloudFlare.
		$this->cloudflare->purge_by_url( $post, $purge_urls, $lang );
	}

	/**
	 * Purge specific Cloudflare cache URLs automatically.
	 *
	 * Used as the callback for both after_rocket_clean_home( $root, $lang ) and after_rocket_clean_files( $urls ).
	 *
	 * @param array|string $urls_or_root Array of URLs to purge when triggered by after_rocket_clean_files, or the home cache root path when triggered by after_rocket_clean_home.
	 * @param string       $lang         The language to purge, only used when triggered by after_rocket_clean_home.
	 *
	 * @return void
	 */
	public function purge_url( $urls_or_root, $lang = '' ) {
		if ( ! current_user_can( 'rocket_purge_cloudflare_cache' ) ) {
			return;
		}

		if ( is_wp_error( $this->cloudflare->check_connection() ) ) {
			return;
		}

		$cf_cache_everything = $this->cloudflare->has_page_rule( 'cache_everything' );

		if ( is_wp_error( $cf_cache_everything ) || ! $cf_cache_everything ) {
			return;
		}

		// after_rocket_clean_files passes an array of URLs directly; after_rocket_clean_home passes a cache root path, so purge the home URL for that lang instead.
		$purge_urls = is_array( $urls_or_root ) ? $urls_or_root : [ get_rocket_i18n_home_url( $lang ) ];
		$purge_urls = array_unique( array_filter( $purge_urls ) );

		if ( empty( $purge_urls ) ) {
			return;
		}

		// Purge CloudFlare.
		$this->cloudflare->purge_by_url( null, $purge_urls, $lang );
	}

	/**
	 * Purge CloudFlare cache.
	 *
	 * @return void
	 */
	public function purge_cache_no_die() {
		if ( ! current_user_can( 'rocket_purge_cloudflare_cache' ) ) {
			return;
		}

		$connection = $this->cloudflare->check_connection();

		if ( is_wp_error( $connection ) ) {
			$cf_purge_result = [
				'result'  => 'error',
				'message' => sprintf(
					// translators: %1$s = <strong>, %2$s = </strong>, %3$s = CloudFare API return message.
					__( '%1$sWP Rocket:%2$s %3$s', 'rocket' ),
					'<strong>',
					'</strong>',
					$connection->get_error_message()
				),
			];

			set_transient( get_current_user_id() . '_cloudflare_purge_result', $cf_purge_result );

			return;
		}

		// Purge CloudFlare.
		$cf_purge        = $this->cloudflare->purge_cloudflare();
		$cf_purge_result = [
			'result'  => 'success',
			'message' => sprintf(
				// translators: %1$s = <strong>, %2$s = </strong>.
				__( '%1$sWP Rocket:%2$s Cloudflare cache successfully purged.', 'rocket' ),
				'<strong>',
				'</strong>'
			),
		];

		if ( is_wp_error( $cf_purge ) ) {
			$cf_purge_result = [
				'result'  => 'error',
				'message' => sprintf(
					// translators: %1$s = <strong>, %2$s = </strong>, %3$s = CloudFare API return message.
					__( '%1$sWP Rocket:%2$s %3$s', 'rocket' ),
					'<strong>',
					'</strong>',
					$cf_purge->get_error_message()
				),
			];
		}

		set_transient( get_current_user_id() . '_cloudflare_purge_result', $cf_purge_result );
	}

	/**
	 * Purge CloudFlare cache.
	 *
	 * @return void
	 */
	public function purge_cache() {
		if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'rocket_purge_cloudflare' ) ) {
			wp_nonce_ays( '' );
		}

		$this->purge_cache_no_die();

		wp_safe_redirect( esc_url_raw( wp_get_referer() ) );
		defined( 'WPMEDIA_IS_TESTING' ) ? wp_die() : exit;
	}

	/**
	 * Set Real IP from CloudFlare.
	 *
	 * @return void
	 */
	public function set_real_ip() {
		Cloudflare::set_ip_rewrite();
	}

	/**
	 * Save Cloudflare dev mode admin option.
	 *
	 * @param string $value New value for Cloudflare dev mode.
	 *
	 * @return string[]
	 */
	private function save_cloudflare_devmode( $value ) {
		$result = $this->cloudflare->set_devmode( $value );

		if ( is_wp_error( $result ) ) {
			return [
				'result'  => 'error',
				// translators: %s is the message returned by the CloudFlare API.
				'message' => sprintf( __( 'Cloudflare development mode error: %s', 'rocket' ), $result->get_error_message() ),
			];
		}

		return [
			'result'  => 'success',
			// translators: %s is the message returned by the CloudFlare API.
			'message' => sprintf( __( 'Cloudflare development mode %s', 'rocket' ), $result ),
		];
	}

	/**
	 * Save Cloudflare cache_level admin option.
	 *
	 * @param string $value New value for Cloudflare cache_level.
	 *
	 * @return string[]
	 */
	private function save_cache_level( $value ) {
		// Set Cache Level to Aggressive.
		$result = $this->cloudflare->set_cache_level( $value );

		if ( is_wp_error( $result ) ) {
			return [
				'result'  => 'error',
				// translators: %s is the message returned by the CloudFlare API.
				'message' => sprintf( __( 'Cloudflare cache level error: %s', 'rocket' ), $result->get_error_message() ),
			];
		}

		$level = $value;

		if ( 'aggressive' === $result ) {
			$level = _x( 'standard', 'Cloudflare caching level', 'rocket' );
		}

		return [
			'result'  => 'success',
			// translators: %s is the caching level returned by the CloudFlare API.
			'message' => sprintf( __( 'Cloudflare cache level set to %s', 'rocket' ), $level ),
		];
	}

	/**
	 * Save Cloudflare rocket loader admin option.
	 *
	 * @param string $value New value for Cloudflare rocket loader.
	 *
	 * @return string[]
	 */
	private function save_rocket_loader( $value ) {
		$result = $this->cloudflare->set_rocket_loader( $value );

		if ( is_wp_error( $result ) ) {
			return [
				'result'  => 'error',
				// translators: %s is the message returned by the CloudFlare API.
				'message' => sprintf( __( 'Cloudflare rocket loader error: %s', 'rocket' ), $result->get_error_message() ),
			];
		}

		return [
			'result'  => 'success',
			// translators: %s is the message returned by the CloudFlare API.
			'message' => sprintf( __( 'Cloudflare rocket loader %s', 'rocket' ), $result ),
		];
	}

	/**
	 * Save Cloudflare browser cache ttl admin option.
	 *
	 * @param int $value New value for Cloudflare browser cache ttl.
	 *
	 * @return string[]
	 */
	private function save_browser_cache_ttl( $value ) {
		$result = $this->cloudflare->set_browser_cache_ttl( $value );

		if ( is_wp_error( $result ) ) {
			return [
				'result'  => 'error',
				// translators: %s is the message returned by the CloudFlare API.
				'message' => sprintf( __( 'Cloudflare browser cache error: %s', 'rocket' ), $result->get_error_message() ),
			];
		}

		return [
			'result'  => 'success',
			// translators: %s is the message returned by the CloudFlare API.
			'message' => sprintf( __( 'Cloudflare browser cache set to %s', 'rocket' ), $result ),
		];
	}

	/**
	 * Save Cloudflare auto settings admin option.
	 *
	 * @param int    $auto_settings New value for Cloudflare auto_settings.
	 * @param string $old_settings Cloudflare cloudflare_old_settings.
	 *
	 * @return array<int, array<string>>
	 */
	private function save_cloudflare_auto_settings( $auto_settings, $old_settings ) {
		$cf_old_settings = explode( ',', $old_settings );

		$result = [];

		// Set Cache Level to Aggressive.
		$cf_cache_level = 0 === $auto_settings ? $cf_old_settings[0] : 'aggressive';
		$result[]       = $this->save_cache_level( $cf_cache_level );

		// Deactivate Rocket Loader to prevent conflicts.
		$cf_rocket_loader = isset( $cf_old_settings[2] ) && 0 === $auto_settings ? $cf_old_settings[2] : 'off';
		$result[]         = $this->save_rocket_loader( $cf_rocket_loader );

		// Set Browser cache to 1 year.
		$cf_browser_cache_ttl = isset( $cf_old_settings[3] ) && 0 === $auto_settings ? $cf_old_settings[3] : 31536000;
		$result[]             = $this->save_browser_cache_ttl( $cf_browser_cache_ttl );

		return $result;
	}

	/**
	 * Update the development mode value on Cloudflare
	 *
	 * @param array $old_value An array of previous values for the settings.
	 * @param array $value     An array of submitted values for the settings.
	 *
	 * @return void
	 */
	public function update_dev_mode( $old_value, $value ) {
		if ( ! current_user_can( 'rocket_manage_options' ) ) {
			return;
		}

		if ( ! isset( $old_value['cloudflare_devmode'], $value['cloudflare_devmode'] ) ) {
			return;
		}

		if ( (int) $old_value['cloudflare_devmode'] === (int) $value['cloudflare_devmode'] ) {
			return;
		}

		$connection = $this->cloudflare->check_connection( $value['cloudflare_zone_id'] );

		if ( is_wp_error( $connection ) ) {
			return;
		}

		$result = [
			'pre' => sprintf(
				'%1$sWP Rocket:%2$s',
				'<strong>',
				'</strong>&nbsp;'
			),
		];
		$update = get_transient( get_current_user_id() . '_cloudflare_update_settings' );

		if ( false !== $update ) {
			$result = $update;
		}

		$result[] = $this->save_cloudflare_devmode( $value['cloudflare_devmode'] );

		set_transient( get_current_user_id() . '_cloudflare_update_settings', $result );
	}

	/**
	 * Save Cloudflare admin options.
	 *
	 * @param array $old_value An array of previous values for the settings.
	 * @param array $value     An array of submitted values for the settings.
	 *
	 * @return void
	 */
	public function save_cloudflare_options( $old_value, $value ) {
		if ( ! current_user_can( 'rocket_manage_options' ) ) {
			return;
		}

		if ( ! isset( $old_value['cloudflare_auto_settings'], $value['cloudflare_auto_settings'] ) ) {
			return;
		}

		if ( (int) $old_value['cloudflare_auto_settings'] === (int) $value['cloudflare_auto_settings'] ) {
			return;
		}

		$connection = $this->cloudflare->check_connection( $value['cloudflare_zone_id'] );

		if ( is_wp_error( $connection ) ) {
			return;
		}

		$result = [
			'pre' => sprintf(
				// translators: %1$s = strong opening tag, %2$s = strong closing tag.
				__( '%1$sWP Rocket:%2$s Optimal settings activated for Cloudflare:', 'rocket' ),
				'<strong>',
				'</strong>'
			) . '<br>',
		];

		if ( 0 === (int) $value['cloudflare_auto_settings'] ) {
			$result['pre'] = sprintf(
				// translators: %1$s = strong opening tag, %2$s = strong closing tag.
				__( '%1$sWP Rocket:%2$s Optimal settings deactivated for Cloudflare, reverted to previous settings:', 'rocket' ),
				'<strong>',
				'</strong>'
			) . '<br>';
		}

		$result = array_merge( $result, $this->save_cloudflare_auto_settings( $value['cloudflare_auto_settings'], $value['cloudflare_old_settings'] ) );

		set_transient( get_current_user_id() . '_cloudflare_update_settings', $result );
	}

	/**
	 * Save Cloudflare old settings when the auto settings option is enabled.
	 *
	 * @param array $value     An array of previous values for the settings.
	 * @param array $old_value An array of submitted values for the settings.
	 *
	 * @return array
	 */
	public function save_cloudflare_old_settings( $value, $old_value ) {
		if ( ! current_user_can( 'rocket_manage_options' ) ) {
			return $value;
		}

		if ( ! isset( $value['cloudflare_auto_settings'], $old_value ['cloudflare_auto_settings'] ) ) {
			return $value;
		}

		if ( $value['cloudflare_auto_settings'] === $old_value ['cloudflare_auto_settings'] ) {
			return $value;
		}

		if ( 0 === (int) $value['cloudflare_auto_settings'] ) {
			return $value;
		}

		$cloudflare_zone_id = key_exists( 'cloudflare_zone_id', $value ) ? $value['cloudflare_zone_id'] : '';

		if ( is_wp_error( $this->cloudflare->check_connection( $cloudflare_zone_id ) ) ) {
			return $value;
		}

		$cf_settings                      = $this->cloudflare->get_settings();
		$value['cloudflare_old_settings'] = ! is_wp_error( $cf_settings )
			? implode( ',', array_filter( $cf_settings ) )
			: '';

		return $value;
	}

	/**
	 * Change the authentication.
	 *
	 * @param array $value     An array of previous values for the settings.
	 * @param array $old_value An array of submitted values for the settings.
	 *
	 * @return mixed
	 */
	public function change_auth( $value, $old_value ) {
		$auth = $this->auth_factory->create( $value );
		$this->cloudflare->change_auth( $auth );
		return $value;
	}

	/**
	 * Delete the transient CF connection status when API Key, Email or Zone ID is changed
	 *
	 * @param array $value     An array of previous values for the settings.
	 * @param array $old_value An array of submitted values for the settings.
	 *
	 * @return array
	 */
	public function delete_connection_transient( $value, $old_value ) {

		$fields = [
			'cloudflare_api_key',
			'cloudflare_email',
			'cloudflare_zone_id',
			'cloudflare_devmode',
			'cloudflare_auto_settings',
			'cloudflare_protocol_rewrite',
		];

		$change = false;

		foreach ( $fields as $field ) {
			$change |= ! isset( $old_value[ $field ], $value[ $field ] ) || $old_value[ $field ] !== $value[ $field ];
		}

		if ( ! $change ) {
			return $value;
		}

		delete_transient( get_current_user_id() . '_cloudflare_update_settings' );
		delete_transient( 'rocket_cloudflare_is_api_keys_valid' );

		return $value;
	}

	/**
	 * Display the error notice.
	 *
	 * @param array $value     An array of previous values for the settings.
	 * @param array $old_value An array of submitted values for the settings.
	 *
	 * @return mixed
	 */
	public function display_settings_notice( $value, $old_value ) {

		if ( ! key_exists( 'cloudflare_zone_id', $value ) ) {
			return $value;
		}

		$connection = $this->cloudflare->check_connection( $value['cloudflare_zone_id'] );

		if ( is_wp_error( $connection ) && function_exists( 'add_settings_error' ) ) {
			add_settings_error( 'general', 'cloudflare_api_key_invalid', __( 'WP Rocket: ', 'rocket' ) . '</strong>' . $connection->get_error_message() . '<strong>', 'error' );
		}

		return $value;
	}

	/**
	 * Remove HTTP protocol on script, link, img and form tags.
	 *
	 * @param string $buffer HTML content.
	 *
	 * @return string
	 */
	public function protocol_rewrite( $buffer ) {
		if ( ! $this->can_protocol_rewrite() ) {
			return $buffer;
		}

		$return = preg_replace( "/(<(script|link|img|form)(?!.*?[\"']\bcanonical\b[\"'])([^>]*)(href|src|action)=[\"'])https?:\\/\\//i", '$1//', $buffer );

		if ( $return ) {
			$buffer = $return;
		}

		return $buffer;
	}

	/**
	 * Remove HTTP protocol on srcset attribute generated by WordPress
	 *
	 * @param array $sources an Array of images sources for srcset.
	 *
	 * @return array
	 */
	public function protocol_rewrite_srcset( $sources ) {
		if ( ! $this->can_protocol_rewrite() ) {
			return $sources;
		}

		if ( empty( $sources ) ) {
			return $sources;
		}

		foreach ( $sources as $i => $source ) {
			$sources[ $i ]['url'] = str_replace( [ 'http:', 'https:' ], '', $source['url'] );
		}

		return $sources;
	}

	/**
	 * Can rewrite protocol
	 *
	 * @return bool
	 */
	private function can_protocol_rewrite(): bool {
		return $this->options->get( 'do_cloudflare', 0 )
		&&
		(
			$this->options->get( 'cloudflare_protocol_rewrite', 0 )
			||
			apply_filters( 'do_rocket_protocol_rewrite', false ) // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
		);
	}

	/**
	 * Add the helper message on the CDN settings.
	 *
	 * @param string[] $addons Name from the addon that requires the helper message.
	 * @return string[]
	 */
	public function add_cdn_helper_message( array $addons ): array {
		$addons[] = 'Cloudflare';
		return $addons;
	}
}
