<?php

namespace Smush\Core\CDN;

use Smush\Core\Media\Attachment_Url_Cache;
use Smush\Core\Settings;
use Smush\Core\Url_Utils;
use Smush\Core\Array_Utils;
use Smush\Core\Keyword_Exclusions;
use WP_Smush;
use WPMUDEV_Dashboard;

class CDN_Helper {
	/**
	 * Static instance
	 *
	 * @var self
	 */
	private static $instance;
	/**
	 * @var Settings
	 */
	private $settings;

	/**
	 * Flag to check if CDN is active.
	 *
	 * @var bool
	 */
	private $cdn_active;
	/**
	 * @var bool|mixed
	 */
	private $status;
	/**
	 * @var string
	 */
	private $cdn_base_url;
	/**
	 * @var Url_Utils
	 */
	private $url_utils;

	private $is_pro;

	/**
	 * Array Utils.
	 *
	 * @var Array_Utils
	 */
	private $array_utils;

	/**
	 * Keyword Exclusions.
	 *
	 * @var Keyword_Exclusions
	 */
	private $keyword_exclusions;

	private $supported_extensions = array(
		'gif',
		'jpg',
		'jpeg',
		'png',
		'webp',
	);

	private $generated_urls = array();

	private $original_urls = array();

	private function __construct() {
		$this->settings    = Settings::get_instance();
		$this->url_utils   = new Url_Utils();
		$this->array_utils = new Array_Utils();
	}

	private function is_url_extension_supported( $url ) {
		$extension = $this->url_utils->get_extension( $url );
		if ( ! $extension ) {
			return false;
		}

		return in_array( $extension, $this->supported_extensions, true );
	}

	private function is_url_scheme_supported( $url ) {
		$url_scheme = $this->url_utils->get_url_scheme( $url );

		return $url_scheme === 'http' || $url_scheme === 'https';
	}

	public function is_supported_url( $url ) {
		if (
			empty( trim( $url ) ) ||
			! $this->is_url_scheme_supported( $url ) ||
			! $this->is_url_extension_supported( $url )
		) {
			return false;
		}

		if ( str_starts_with( $url, content_url() ) ) {
			return true;
		}

		$uploads            = $this->get_cdn_custom_uploads_dir();
		$base_url_available = isset( $uploads['baseurl'] );
		if ( $base_url_available && str_starts_with( $url, $uploads['baseurl'] ) ) {
			return true;
		}

		$mapped_domain = $this->check_mapped_domain();
		if ( $mapped_domain ) {
			$url           = set_url_scheme( $url, 'http' );
			$mapped_domain = set_url_scheme( $mapped_domain, 'http' );
			return str_starts_with( $url, $mapped_domain );
		}

		return false;
	}

	/**
	 * Support for domain mapping plugin.
	 *
	 * @since 3.1.1
	 */
	private function check_mapped_domain() {
		if ( ! is_multisite() ) {
			return false;
		}

		if ( ! defined( 'DOMAINMAP_BASEFILE' ) ) {
			return false;
		}

		$domain = wp_cache_get( 'smush_mapped_site_domain', 'smush' );

		if ( ! $domain ) {
			global $wpdb;

			$domain = $wpdb->get_var(
				$wpdb->prepare(
					"SELECT domain FROM {$wpdb->base_prefix}domain_mapping WHERE blog_id = %d ORDER BY id LIMIT 1",
					get_current_blog_id()
				)
			); // Db call ok.

			if ( null !== $domain ) {
				wp_cache_add( 'smush_mapped_site_domain', $domain, 'smush' );
			}
		}

		return $domain;
	}

	private function get_cdn_custom_uploads_dir() {
		/**
		 * There are chances for a custom uploads directory using UPLOADS constant.
		 *
		 * But some security plugins (for example, WP Hide & Security Enhance) will allow replacing paths via Nginx/Apache
		 * rules. So for this reason, we don't want the path to be replaced everywhere with the custom UPLOADS constant,
		 * we just want to let the user redefine it here, in the CDN.
		 *
		 * @param array $uploads {
		 *     Array of information about the upload directory.
		 *
		 * @type string $path Base directory and subdirectory or full path to upload directory.
		 * @type string $url Base URL and subdirectory or absolute URL to upload directory.
		 * @type string $subdir Subdirectory if uploads use year/month folders option is on.
		 * @type string $basedir Path without subdir.
		 * @type string $baseurl URL path without subdir.
		 * @type string|false $error False or error message.
		 * }
		 *
		 * Usage (replace /wp-content/uploads/ with /media/ directory):
		 *
		 * add_filter(
		 *     'smush_cdn_custom_uploads_dir',
		 *     function( $uploads ) {
		 *         $uploads['baseurl'] = 'https://example.com/media';
		 *         return $uploads;
		 *     }
		 * );
		 * @since 3.4.0
		 *
		 */
		return apply_filters( 'smush_cdn_custom_uploads_dir', wp_get_upload_dir() );
	}

	/**
	 * Static instance getter
	 */
	public static function get_instance() {
		if ( empty( self::$instance ) ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	public function is_cdn_active() {
		if ( is_null( $this->cdn_active ) ) {
			$this->cdn_active = $this->check_is_cdn_active();
		}
		return $this->cdn_active;
	}

	private function check_is_cdn_active() {
		$status = $this->get_cdn_status();

		return isset( $status->cdn_enabled ) && $status->cdn_enabled;
	}

	/**
	 * Only for tests, don't use in actual code
	 */
	public function set_cdn_active( $cdn_active ) {
		$this->cdn_active = $cdn_active;
	}

	public function get_cdn_status() {
		if ( is_null( $this->status ) ) {
			$this->status = $this->check_cdn_status();
		}
		return $this->status;
	}

	private function check_cdn_status() {
		// The CDN module needs to be active
		if ( ! $this->settings->is_cdn_active() ) {
			return false;
		}

		// All these are members only feature.
		if ( ! $this->is_pro() ) {
			return false;
		}

		// Disable CDN on staging.
		if ( $this->is_wpmudev_staging_environment() ) {
			return false;
		}

		// CDN is not enabled and not active.
		$status = $this->get_cdn_status_setting();
		if ( ! $status ) {
			return false;
		}

		return $status;
	}

	/**
	 * Only for tests, don't use in actual code
	 */
	public function set_cdn_status( $cdn_status ) {
		$this->status = $cdn_status;
	}

	/**
	 * @return bool
	 */
	private function is_wpmudev_staging_environment() {
		return isset( $_SERVER['WPMUDEV_HOSTING_ENV'] ) && 'staging' === $_SERVER['WPMUDEV_HOSTING_ENV'];
	}

	/**
	 * @return bool
	 */
	private function is_pro() {
		if ( is_null( $this->is_pro ) ) {
			$this->is_pro = $this->check_if_pro();
		}

		return $this->is_pro;
	}

	/**
	 * Only for tests, don't use in actual code
	 */
	public function set_is_pro( $is_pro ) {
		$this->is_pro = $is_pro;
	}

	private function check_if_pro() {
		/**
		 * Do not allow enabling CDN for sites that are not registered on the Hub
		 * Taken from 6e13e2f0
		 */
		return WP_Smush::is_pro()
		       // CDN will not work if there is no dashboard plugin installed.
		       && class_exists( 'WPMUDEV_Dashboard' )
		       // CDN will not work if site is not registered with the dashboard.
		       && WPMUDEV_Dashboard::$api->has_key();
	}

	public function get_cdn_base_url() {
		if ( is_null( $this->cdn_base_url ) ) {
			$this->cdn_base_url = $this->prepare_cdn_base_url();
		}

		return $this->cdn_base_url;
	}

	/**
	 * @return bool|mixed
	 */
	public function get_cdn_status_setting() {
		return $this->settings->get_setting( 'wp-smush-cdn_status' );
	}

	private function prepare_cdn_base_url() {
		$status  = $this->get_cdn_status();
		$site_id = absint( $status->site_id );

		return trailingslashit( "https://{$status->endpoint_url}/{$site_id}" );
	}

	public function get_cdn_status_string() {
		if ( ! $this->settings->is_cdn_active() ) {
			return 'disabled';
		}

		$cdn = $this->get_cdn_status_setting();
		if ( ! $cdn ) {
			return 'disabled';
		}

		if ( isset( $cdn->cdn_enabling ) && $cdn->cdn_enabling ) {
			return 'activating';
		}

		$plan      = isset( $cdn->bandwidth_plan ) ? $cdn->bandwidth_plan : 10;
		$bandwidth = isset( $cdn->bandwidth ) ? $cdn->bandwidth : 0;

		$percentage = round( 100 * $bandwidth / 1024 / 1024 / 1024 / $plan );

		if ( $percentage > 100 || 100 === (int) $percentage ) {
			return 'overcap';
		} elseif ( 90 <= (int) $percentage ) {
			return 'upgrade';
		}

		return 'enabled';
	}

	/**
	 * Generate CDN url from given image url.
	 *
	 * @param string $src Image url.
	 * @param array $args Query parameters.
	 *
	 * @return string
	 * @since 3.0
	 *
	 */
	public function generate_cdn_url( $original_url, $args = array() ) {
		// Do not continue in case we try this when cdn is disabled.
		if ( ! $this->is_cdn_active() ) {
			return $original_url;
		}

		$args             = wp_parse_args( $this->get_cdn_parameters(), $args );
		$original_url     = trim( $original_url );
		$original_url_key = $this->get_original_url_key( $original_url, $args );
		// If the URL is cached then return the cached value
		if ( isset( $this->generated_urls[ $original_url_key ] ) ) {
			return $this->generated_urls[ $original_url_key ];
		}

		/**
		 * Filter hook to alter image src before going through cdn.
		 *
		 * @param string $original_url Image src.
		 *
		 * @see smush_image_src_before_cdn filter if you need earlier access with the image element.
		 *
		 * @since 3.4.0
		 */
		$original_url = apply_filters( 'smush_filter_generate_cdn_url', $original_url );

		// Support for WP installs in subdirectories: remove the site url and leave only the file path.
		$path = str_replace( $this->get_site_url(), '', $original_url );

		// Parse url to get all parts.
		$url_parts = wp_parse_url( $path );

		// If path not found, do not continue.
		if ( empty( $url_parts['path'] ) ) {
			return $original_url;
		}

		// Replace base url with cdn base.
		$url = $this->get_cdn_base_url() . ltrim( $url_parts['path'], '/' );

		// Now we need to add our CDN parameters for resizing.
		$generated_cdn_url = add_query_arg( $args, $url );

		$this->generated_urls[ $original_url_key ] = $generated_cdn_url;

		$this->original_urls[ $generated_cdn_url ] = $original_url;

		return $generated_cdn_url;
	}

	private function get_site_url() {
		$site_url = get_site_url();
		$home_url = get_home_url();
		if ( $site_url === $home_url ) {
			return $site_url;
		}

		$content_url = content_url();
		$root_url    = trailingslashit( dirname( $content_url ) );
		if (
			false === strpos( $root_url, $site_url ) &&
			false !== strpos( $root_url, $home_url )
		) {
			$site_url = $home_url;
		}

		return $site_url;
	}

	private function get_cdn_parameters() {
		$next_gen_cdn        = $this->settings->get_cdn_next_gen_conversion_mode();
		$lossy_level_setting = $this->settings->get_lossy_level_setting();
		$strip_exif          = $this->settings->get( 'strip_exif' );

		$cdn_params = array(
			'lossy' => $lossy_level_setting,
			'strip' => (int) $strip_exif,
		);

		if ( Settings::AVIF_CDN_MODE === $next_gen_cdn ) {
			$cdn_params['avif'] = 1;
		} elseif ( Settings::WEBP_CDN_MODE === $next_gen_cdn ) {
			$cdn_params['webp'] = 1;
		} else {
			$cdn_params['webp'] = 0;
		}

		return $cdn_params;
	}

	/**
	 * @param $image_markup
	 *
	 * @return string|false String in the form of width x height e.g. "400x400" or false if it couldn't be guessed.
	 */
	public function guess_dimensions_from_image_markup( $image_markup ) {
		// Get registered image sizes.
		$image_sizes = WP_Smush::get_instance()->core()->image_dimensions();

		// Find the width and height attributes.
		$width  = false;
		$height = false;

		// Try to get the width and height from img tag.
		if ( preg_match( '/width=["|\']?(\b[[:digit:]]+(?!%)\b)["|\']?/i', $image_markup, $width_string ) ) {
			$width = $width_string[1];
		}

		if ( preg_match( '/height=["|\']?(\b[[:digit:]]+(?!%)\b)["|\']?/i', $image_markup, $height_string ) ) {
			$height = $height_string[1];
		}

		$size = array();

		// Detect WP registered image size from HTML class.
		if ( preg_match( '/size-([^"\'\s]+)[^"\']*["|\']?/i', $image_markup, $size ) ) {
			$size = array_pop( $size );

			if ( ! array_key_exists( $size, $image_sizes ) ) {
				return false;
			}

			// This is probably a correctly sized thumbnail - no need to resize.
			if ( (int) $width === $image_sizes[ $size ]['width'] || (int) $height === $image_sizes[ $size ]['height'] ) {
				return false;
			}

			// If this size exists in registered sizes, add argument.
			if ( 'full' !== $size ) {
				return (int) $image_sizes[ $size ]['width'] . 'x' . (int) $image_sizes[ $size ]['height'];
			}
		} else {
			// It's not a registered thumbnail size.
			if ( $width && $height ) {
				return (int) $width . 'x' . (int) $height;
			}
		}

		return false;
	}

	public function set_settings( $settings ) {
		$this->settings = $settings;
	}

	public function skip_image_url( $url, $image_markup = false ) {
		$image_markup = ! empty( $image_markup ) && is_string( $image_markup ) ? $image_markup : '';
		$is_skipped   = $this->keyword_exclusions()->is_url_excluded( $url );

		return apply_filters( 'smush_skip_image_from_cdn', $is_skipped, $url, $image_markup );
	}

	/**
	 * Keyword Exclusions.
	 *
	 * @return Keyword_Exclusions
	 */
	public function keyword_exclusions() {
		if ( ! $this->keyword_exclusions ) {
			$this->keyword_exclusions = new Keyword_Exclusions( $this->get_excluded_keywords() );
		}

		return $this->keyword_exclusions;
	}

	public function get_excluded_keywords() {
		$excluded_keywords = $this->array_utils->get_array_value( $this->get_cdn_advanced_settings(), 'excluded-keywords' );
		$excluded_keywords = $this->array_utils->ensure_array( $excluded_keywords );

		return apply_filters( 'wp_smush_cdn_excluded_keywords', array_unique( $excluded_keywords ) );
	}

	private function get_cdn_advanced_settings() {
		return $this->settings->get_setting( 'wp-smush-cdn-advanced-settings', $this->get_default_cdn_advanced_settings() );
	}

	private function get_default_cdn_advanced_settings() {
		$default_exclusion_keywords = array(
			'@placeholder',
			'@dummy',
			'@loading.gif',
		);

		return array(
			'excluded-keywords' => $default_exclusion_keywords,
		);
	}

	public function is_rest_request() {
		return defined( 'REST_REQUEST' ) && REST_REQUEST;
	}

	public function is_dynamic_sizes_active() {
		if ( ! $this->is_cdn_active() ) {
			return false;
		}

		return $this->settings->get( 'cdn_dynamic_sizes' );
	}

	public function is_cdn_url( $url ) {
		$cdn_base_url = $this->get_cdn_base_url();
		if ( empty( $cdn_base_url ) ) {
			return false;
		}

		// Check if the URL starts with the CDN base URL.
		return str_starts_with( $url, $cdn_base_url );
	}

	public function get_original_url( $cdn_url ) {
		if ( isset( $this->original_urls[ $cdn_url ] ) ) {
			return $this->original_urls[ $cdn_url ];
		}

		return false;
	}

	/**
	 * @param $original_url
	 * @param array $args
	 *
	 * @return string
	 */
	private function get_original_url_key( $original_url, array $args ): string {
		$key = md5( $original_url );
		if ( $args ) {
			$key .= '-' . $this->array_utils->array_hash( $args );
		}

		return $key;
	}
}
