<?php

namespace EasyCriticalCSS;

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

class Helpers {
	/**
	 * Checks if the current request is a WordPress heartbeat request.
	 *
	 * @return bool
	 */
	public static function is_heartbeat_request() {
		return defined( 'DOING_AJAX' ) && DOING_AJAX && isset( $_POST['action'] ) && $_POST['action'] === 'heartbeat'; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- No need for nonce verification as we are using this to check for heartbeat requests.
	}

	/**
	 * Checks if data is a valid JSON string.
	 *
	 * @param mixed $data Data to be checked
	 * @return boolean Is data valid JSON
	 */
	public static function is_valid_json( $data ) {
		if ( function_exists( 'json_validate' ) ) {
			return json_validate( $data );
		}

		$decoded_data = json_decode( $data );
		return $decoded_data !== null || json_last_error() === JSON_ERROR_NONE;
	}

	public static function get_freemius_data( $option_name, $fs_key_path ) {
		// Check if the value is already stored.
		$value = get_option( $option_name );

		// If not stored, pull it from the fs_accounts option.
		if ( empty( $value ) ) {
			$fs_data = get_option( 'fs_accounts' );

			// Extract the specified value through the nested data structure.
			$keys    = explode( '.', $fs_key_path );
			$current = $fs_data;

			foreach ( $keys as $key ) {
				if ( is_array( $current ) && isset( $current[ $key ] ) ) {
					$current = $current[ $key ];
				} elseif ( is_object( $current ) && isset( $current->$key ) ) {
					$current = $current->$key;
				} else {
					$current = false;
					break;
				}
			}

			// If found, calls from the future thank us.
			if ( ! empty( $current ) ) {
				update_option( $option_name, $current );
				$value = $current;
			}
		}

		// Return the value or empty handed.
		return ! empty( $value ) ? $value : false;
	}

	public static function get_uid() {
		return self::get_freemius_data( 'fs_plugin_17865_uid', 'unique_id' );
	}

	public static function get_api_key() {
		$license = easy_cc_fs()->_get_license();
		if ( ! empty( $license->secret_key ) ) {
			return $license->secret_key;
		}

		return null;
	}

	public static function get_install_id() {
		return self::get_freemius_data( 'fs_plugin_17865_install_id', 'sites.easy-critical-css.id' );
	}

	public static function get_url_hash( $identifier ) {
		// Check if this is already a hash.
		if ( preg_match( '/^[a-f0-9]{32}$/', $identifier ) ) {
			return $identifier;
		}

		$url = $identifier;
		if ( is_numeric( $identifier ) ) {
			$url = get_permalink( (int) $identifier ) ? get_permalink( (int) $identifier ) : home_url( "/?p=$identifier" );
		}

		// Convert to hash.
		return md5( $url );
	}

	public static function get_url_from_identifier( $identifier ) {
		// Check if this is already a URL.
		if ( filter_var( $identifier, FILTER_VALIDATE_URL ) ) {
			return $identifier;
		}

		// If it's numerical, it's probably a post ID, so get the permalink.
		if ( is_numeric( $identifier ) ) {
			$permalink = get_permalink( (int) $identifier );
			if ( ! empty( $permalink ) ) {
				return $permalink;
			}
		}

		// Look it up in the DB.
		$row = Database::get_row( $identifier );
		if ( ! empty( $row['page_url'] ) ) {
			return (int) $row['page_url'];
		}

		// No URL found.
		return null;
	}

	public static function get_post_id_from_identifier( $identifier ) {
		// Check if this is already a number (post id).
		if ( is_numeric( $identifier ) ) {
			return (int) $identifier;
		}

		// Look it up in the DB.
		$row = Database::get_row( $identifier );
		if ( ! empty( $row['post_id'] ) ) {
			return (int) $row['post_id'];
		}

		// Can we find it from the URL?
		$url = self::get_url_from_identifier( $identifier );
		if ( ! empty( $url ) ) {
			$post_id = url_to_postid( $url ); // Fastest method.
			if ( $post_id ) {
				return $post_id;
			}

			// Fallback for hierarchical pages.
			$path = str_replace( home_url(), '', $url );
			$post = get_page_by_path( $path, OBJECT, get_post_types() );
			if ( ! empty( $post ) ) {
				return (int) $post->ID;
			}
		}

		// No ID found.
		return null;
	}

	public static function is_excluded_css_file( $file_url ) {
		$excluded_files = Settings::get_global_excluded_css_files();

		if ( empty( $file_url ) || empty( $excluded_files ) ) {
			return false;
		}

		foreach ( $excluded_files as $excluded ) {
			if ( strpos( $file_url, $excluded ) !== false ) {
				return true;
			}
		}

		return false;
	}

	public static function get_remaining_time( $initial_time, $minutes_to_check = 30 ) {
		// If we have no time, consider it within
		if ( empty( $initial_time ) ) {
			return true;
		}

		$initial_time = strtotime( get_gmt_from_date( $initial_time ) );
		$current_time = time();

		$remaining_time = ( $minutes_to_check * MINUTE_IN_SECONDS ) - ( $current_time - $initial_time );

		return ceil( $remaining_time / 60 );
	}

	public static function is_local_site( $host, $debug = false ) {
		// Check transient.
		$cached = Timed_Option::get( 'is_local_site' );
		if ( ! $debug && $cached !== false ) {
			return (bool) $cached;
		}

		$is_local     = false;
		$debug_output = [];

		// Do we fail WP's local test?
		if ( wp_get_environment_type() === 'local' ) {
			$is_local = true;

			$debug_output['wp_get_environment_type'] = true;
		}

		// Let's play it safe and not have a case issue.
		$host = strtolower( $host );

		// Are we a localhost?
		if ( in_array( $host, [ 'localhost', '127.0.0.1', '::1' ], true ) ) {
			$is_local = true;

			$debug_output['localhost'] = true;
		}

		// Are we a reserved TLD for local development?
		if ( preg_match( '/\.(local|test|localhost|invalid)$/', $host ) ) {
			$is_local = true;

			$debug_output['tld'] = true;
		}

		// Is the site running from a known local environment variable
		if ( function_exists( 'getenv' ) && getenv( 'WP_ENV' ) === 'local' ) {
			$is_local = true;

			$debug_output['env'] = true;
		}

		/**
		 * Filters whether the site is considered local.
		 *
		 * @since 1.0.0
		 *
		 * @param boolean $is_site_local Whether the site is considered local.
		 * @param string $host Hostname being checked.
		 */
		$is_local = (bool) apply_filters( 'easy_cc_is_local_site', $is_local, $host );

		// We need to store both true and false values as a transient, but only if no debug.
		if ( ! $debug ) {
			Timed_Option::set( 'is_local_site', $is_local ? '1' : '0', 6 * HOUR_IN_SECONDS );
		} else {
			// Give more info if debug.
			$debug_output['is_local'] = $is_local;

			return $debug_output;
		}

		return $is_local;
	}

	public static function has_auto_mode_api_credentials() {
		return ! empty( self::get_uid() )
			&& ! empty( self::get_api_key() )
			&& ! empty( self::get_install_id() );
	}

	public static function get_auto_mode_status_reason( $status ) {
		if ( ! empty( $status['all_ok'] ) ) {
			return 'ready';
		}

		// Prioritize a license issue first because it fully blocks auto mode.
		if ( empty( $status['active_key'] ) ) {
			return 'api_expired';
		}

		if ( isset( $status['api_credentials_ok'] ) && empty( $status['api_credentials_ok'] ) ) {
			return 'no_api_key';
		}

		if ( empty( $status['local_ok'] ) ) {
			return 'site_private';
		}

		if ( empty( $status['rest_api_ok'] ) ) {
			return 'rest_unreachable';
		}

		return 'unknown_unreachable';
	}

	public static function get_auto_mode_status( $force_refresh = false ) {
		// Check transient.
		$cached = Timed_Option::get( 'auto_mode_status' );
		if ( ! $force_refresh && $cached !== false ) {
			return $cached;
		}

		$status = [
			'active_key'         => false,
			'api_credentials_ok' => self::has_auto_mode_api_credentials(),
			'local_ok'           => ! self::is_local_site( wp_parse_url( site_url(), PHP_URL_HOST ) ),
			'rest_api_ok'        => false,
			'all_ok'             => false,
			'reason'             => 'unknown_unreachable',
		];

		// Only run full status check if there's an active API key.
		if ( empty( easy_cc_fs()->has_active_valid_license() ) ) {
			$status['reason'] = self::get_auto_mode_status_reason( $status );
			Timed_Option::set( 'auto_mode_status', $status, 6 * HOUR_IN_SECONDS );

			return $status;
		}

		$status['active_key']  = true;
		$status['rest_api_ok'] = $status['api_credentials_ok'] ? API_Service::test_receive_endpoint() : false;

		// Overall ready state requires an active key, non-local install, and REST API reachability.
		$status['all_ok'] = $status['active_key'] && $status['local_ok'] && $status['rest_api_ok'];
		$status['reason'] = self::get_auto_mode_status_reason( $status );

		Timed_Option::set( 'auto_mode_status', $status, 6 * HOUR_IN_SECONDS );

		return $status;
	}

	public static function can_run_auto_generation() {
		$status = self::get_auto_mode_status();
		return $status['all_ok'];
	}

	public static function is_rest_api_reachable( $debug = false ) {
		// Check transient.
		$cached = Timed_Option::get( 'is_rest_api_reachable' );
		if ( ! $debug && $cached !== false ) {
			return (bool) $cached;
		}

		$auto_mode_status = self::get_auto_mode_status();
		$is_reachable     = $auto_mode_status['rest_api_ok'];

		// We need to store both true and false values as a transient, but only if no debug.
		if ( ! $debug ) {
			Timed_Option::set( 'is_rest_api_reachable', $is_reachable ? '1' : '0', 6 * HOUR_IN_SECONDS );
		} else {
			// Give more info if debug.
			return [
				'is_reachable' => $is_reachable,
				'url'          => rest_url( 'easy-critical-css/v1/receive-test' ),
				'checked_via'  => 'test_receive_endpoint',
			];
		}

		return $is_reachable;
	}

	public static function esc_css( $css ) {
		// Remove any script/style/etc. tags that might've been injected
		$css = preg_replace( '/<\/?(script|style|iframe|html|body|head)[^>]*>/i', '', $css );

		// Trim and normalize whitespace
		return trim( $css );
	}

	public static function css_looks_like_url( $css ) {
		if ( ! is_string( $css ) ) {
			return false;
		}

		// Trim whitespace.
		$trimmed = ltrim( $css );
		if ( $trimmed === '' ) {
			return false;
		}

		// Pull first 10 characters.
		$first10 = substr( $trimmed, 0, 10 );

		// Quick check for http(s).
		if ( stripos( $first10, 'http://' ) === 0 || stripos( $first10, 'https://' ) === 0 ) {
			return true;
		}

		// Check leading slash but not start-of-comment (/*)
		if ( $first10[0] === '/' && ( ! isset( $first10[1] ) || $first10[1] !== '*' ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Parses a newline-separated string into a clean deduped array.
	 *
	 * @param string $selector_string Raw newline-separated string.
	 *
	 * @return array Cleaned array of selectors.
	 */
	public static function parse_newline_list( $selector_string ) {
		$seen   = [];
		$result = [];

		$selectors = explode( "\n", $selector_string );
		foreach ( $selectors as $selector ) {
			$item = trim( $selector );
			if ( $item === '' || isset( $seen[ $item ] ) ) {
				continue;
			}
			$seen[ $item ] = true;
			$result[]      = $item;
		}

		return $result;
	}

	/**
	 * Gets the list of URL params to exclude from URLs.
	 *
	 * @return array List of URL params to exclude.
	 */
	public static function get_excluded_url_params() {
		$excluded_url_params = [
			'breeze_check_cache',
			'rnd',
			'no-cache',
			'timestamp',
			'sucurianticache',
			'wp_scrape_key',
			'wp_scrape_nonce',
		];

		/**
		 * Filters the list of excluded URL parameters when generating CSS.
		 *
		 * @since 1.0.0
		 *
		 * @param array $excluded_url_params List of query paramters that should be excluded.
		 */
		return apply_filters( 'easy_cc_excluded_url_params', $excluded_url_params );
	}
}
