<?php
/**
 * GTM Kit plugin file.
 *
 * @package GTM Kit
 */

namespace TLA_Media\GTM_Kit\Common;

use TLA_Media\GTM_Kit\Integration\WooCommerce;
use TLA_Media\GTM_Kit\Options\Options;

/**
 * Class for common utilities.
 */
final class Util {

	/**
	 * Instance of Options
	 *
	 * @var Options
	 */
	public Options $options;

	/**
	 * Instance of RestAPIServer
	 *
	 * @var RestAPIServer
	 */
	public RestAPIServer $rest_api_server;

	/**
	 * Asset path
	 *
	 * @var string
	 */
	public string $asset_path;

	/**
	 * Asset URL
	 *
	 * @var string
	 */
	public string $asset_url;

	/**
	 * API namespace.
	 *
	 * @var string
	 */
	private string $api_namespace = '/api/v1';

	/**
	 * API host.
	 *
	 * @var string
	 */
	private string $api_host;

	/**
	 * Constructor.
	 *
	 * @param Options       $options Instance of Options.
	 * @param RestAPIServer $rest_api_server Instance of RestAPIServer.
	 * @param string        $path The plugin path.
	 * @param string        $url The plugin URL.
	 */
	public function __construct( Options $options, RestAPIServer $rest_api_server, string $path = '', string $url = '' ) {
		if ( empty( $path ) ) {
			$path = GTMKIT_PATH;
		}
		if ( empty( $url ) ) {
			$url = GTMKIT_URL;
		}

		$this->options         = $options;
		$this->rest_api_server = $rest_api_server;
		$this->asset_path      = $path . 'assets/';
		$this->asset_url       = $url . 'assets/';

		if ( $options->is_const_enabled() && defined( 'GTMKIT_API_HOST' ) ) {
			$this->api_host = GTMKIT_API_HOST;
		} else {
			$this->api_host = 'https://api.gtmkit.com';
		}
	}

	/**
	 * Get the site data
	 *
	 * @param array<string, mixed> $options The options.
	 * @param bool                 $anonymize Anonymize the data.
	 *
	 * @return array<string, mixed>
	 */
	public function get_site_data( array $options, bool $anonymize = true ): array {

		global $wp_version;

		$data = [];
		$data = $this->set_site_data( $data, $options, $wp_version, $anonymize );

		$data = $this->add_active_plugin_and_version( 'gtm-kit/gtm-kit.php', 'gtmkit_version', $data, $anonymize );

		$data['ecommerce'] = false;

		$ecommerce_plugins = [
			'woocommerce/woocommerce.php' => 'woocommerce_version',
			'easy-digital-downloads/easy-digital-downloads.php' => 'edd_version',
			'easy-digital-downloads-pro/easy-digital-downloads.php' => 'edd-pro_version',
		];

		foreach ( $ecommerce_plugins as $plugin => $key ) {
			$data = $this->add_active_plugin_and_version( $plugin, $key, $data, $anonymize, true );
		}
		$data['locale'] = explode( '_', get_locale() )[0];

		if ( $anonymize ) {
			$data = $this->add_shared_data( $data, $wp_version );
		} else {
			$purchase_event_recorded = false;

			// Check if WooCommerce logs contain GTM Kit purchase events.
			if ( class_exists( 'WC_Log_Handler_File' ) ) {
				$log_files = \WC_Log_Handler_File::get_log_files();
				// Check if any log file starts with 'gtmkit-purchase'.
				foreach ( $log_files as $log_file ) {
					if ( strpos( $log_file, 'gtmkit-purchase' ) === 0 ) {
						$purchase_event_recorded = true;
						break;
					}
				}
			}
			$data['support_data']['purchase_event_recorded'] = $purchase_event_recorded;
			$data['support_data']['site_url']                = site_url();
			if ( function_exists( 'WC' ) ) {
				$data['support_data']['pages'] = WooCommerce::instance()->get_pages_property( [] )['pages'];
			}
		}

		return $data;
	}

	/**
	 * Set the site data
	 *
	 * @param array<string, mixed> $data Current data.
	 * @param array<string, mixed> $options The options.
	 * @param string               $wp_version The WordPress version.
	 * @param bool                 $anonymize Anonymize the data.
	 *
	 * @return array<string, mixed>
	 */
	private function set_site_data( array $data, array $options, string $wp_version, bool $anonymize ): array {
		$data['options']           = ( $anonymize ) ? $this->anonymize_options( $options ) : $options;
		$data['web_server']        = $this->get_web_server();
		$data['php_version']       = $this->shorten_version( phpversion() );
		$data['wordpress_version'] = $this->shorten_version( $wp_version );
		// `get_template()` returns the parent template's directory name for
		// child themes and the theme's own stylesheet name otherwise, never
		// empty and never translated, so it doesn't JIT-load the active
		// theme's text domain. The previous `get( 'Template' ) ?: get( 'Name' )`
		// fallback fired translation loading for parent themes (where the raw
		// Template header is empty), which WP 6.7+ logs as `_doing_it_wrong`
		// when called before `init`.
		$data['current_theme']  = ucwords( wp_get_theme()->get_template() );
		$data['active_plugins'] = $this->get_active_plugins();
		$data['multisite']      = \is_multisite();

		return $data;
	}

	/**
	 * Add shared data
	 *
	 * @param array<string, mixed> $data Current data.
	 * @param string               $wp_version The WordPress version.
	 *
	 * @return array<string, mixed>
	 */
	private function add_shared_data( array $data, string $wp_version ): array {
		$data['shared_data'] = [
			1 => [
				'label' => __( 'Server type:', 'gtm-kit' ),
				'value' => $this->get_web_server(),
				'tag'   => 'code',
			],
			2 => [
				'label' => __( 'PHP version number:', 'gtm-kit' ),
				'value' => $this->shorten_version( phpversion() ),
				'tag'   => 'code',
			],
			3 => [
				'label' => __( 'WordPress version number:', 'gtm-kit' ),
				'value' => $this->shorten_version( $wp_version ),
				'tag'   => 'code',
			],
			4 => [
				'label' => __( 'WordPress multisite:', 'gtm-kit' ),
				'value' => ( \is_multisite() ) ? __( 'Yes', 'gtm-kit' ) : __( 'No', 'gtm-kit' ),
				'tag'   => 'code',
			],
			5 => [
				'label' => __( 'Current theme:', 'gtm-kit' ),
				// See `set_site_data()` for why `get_template()` replaces the
				// `get( 'Template' ) ?: get( 'Name' )` fallback.
				'value' => ucwords( wp_get_theme()->get_template() ),
				'tag'   => 'code',
			],
			6 => [
				'label' => __( 'Current site language:', 'gtm-kit' ),
				'value' => explode( '_', get_locale() )[0],
				'tag'   => 'code',
			],
			7 => [
				'label' => __( 'Active plugins:', 'gtm-kit' ),
				'value' => __( 'Plugin name and version of all active plugins', 'gtm-kit' ),
				'tag'   => 'em',
			],
			8 => [
				'label' => __( 'Anonymized GTM Kit settings:', 'gtm-kit' ),
				'value' => __( 'Which GTM Kit settings are active', 'gtm-kit' ),
				'tag'   => 'em',
			],
		];

		return $data;
	}

	/**
	 * Gets names of all active plugins.
	 *
	 * @return array<int, string> An array of active plugins names.
	 */
	public function get_active_plugins(): array {

		self::load_plugin_api();

		$plugins        = [];
		$active_plugins = array_intersect_key( \get_plugins(), array_flip( array_filter( array_keys( \get_plugins() ), 'is_plugin_active' ) ) );

		foreach ( $active_plugins as $plugin ) {
			$plugins[] = $plugin['Name'];
		}

		return $plugins;
	}

	/**
	 * Add plugin to array if active.
	 *
	 * @param string               $plugin The plugin slug.
	 * @param string               $key The key.
	 * @param array<string, mixed> $data The data.
	 * @param bool                 $shorten Shorten the version number or not.
	 * @param bool                 $is_ecommerce Whether this is an ecommerce plugin.
	 *
	 * @return array<string, mixed> An array of active plugins names.
	 */
	public function add_active_plugin_and_version( string $plugin, string $key, array $data, bool $shorten = true, bool $is_ecommerce = false ): array {

		if ( \is_plugin_active( $plugin ) ) {
			$version      = \get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin )['Version'];
			$data[ $key ] = ( $shorten ) ? $this->shorten_version( $version ) : $version;
			if ( $is_ecommerce ) {
				$data['ecommerce'] = true;
			}
		}

		return $data;
	}

	/**
	 * Anonymize options
	 *
	 * @param array<string, mixed> $options The options.
	 *
	 * @return array<string, mixed>
	 */
	public function anonymize_options( array $options ): array {

		unset( $options['general']['gtm_id'] );

		$anonymize_general_options = [ 'datalayer_name', 'sgtm_domain', 'sgtm_container_identifier' ];

		foreach ( $anonymize_general_options as $option ) {
			if ( ! empty( $options['general'][ $option ] ) ) {
				$options['general'][ $option ] = $option;
			}
		}

		return $options;
	}

	/**
	 * Shorten version number
	 *
	 * @param string $version The version number.
	 *
	 * @return string
	 */
	public function shorten_version( string $version ): string {
		return preg_replace( '@^(\d+\.\d+).*@', '\1', $version );
	}

	/**
	 * Get web server
	 *
	 * @return string
	 */
	public function get_web_server(): string {

		global $is_nginx, $is_apache, $is_iis7, $is_IIS;

		if ( $is_nginx ) {
			$web_server = 'NGINX';
		} elseif ( $is_apache ) {
			$web_server = 'Apache';
		} elseif ( $is_iis7 ) {
			$web_server = 'IIS 7';
		} elseif ( $is_IIS ) {
			$web_server = 'IIS';
		} else {
			$web_server = 'Unknown';
		}

		return $web_server;
	}

	/**
	 * Get the plugin version
	 *
	 * @return string
	 */
	public function get_plugin_version(): string {
		return ( \wp_get_environment_type() === 'local' ) ? time() : GTMKIT_VERSION;
	}

	/**
	 * Enqueue script in build
	 *
	 * @param string                      $handle The script handle.
	 * @param string                      $script The script name.
	 * @param bool                        $has_asset_file If the script has an asset file or not.
	 * @param array<int, string>          $deps The script dependencies.
	 * @param array<string, string|false> $args The loading strategy.
	 *
	 * @return void
	 */
	public function enqueue_script( string $handle, string $script, bool $has_asset_file = false, array $deps = [], array $args = [
		'strategy'  => 'defer',
		'in_footer' => false,
	] ): void {

		$ver = $this->get_plugin_version();

		if ( $has_asset_file ) {
			$file = $this->asset_path . substr_replace( $script, '.asset.php', - strlen( '.js' ) );
			$file = \realpath( $this->asset_path . substr_replace( $script, '.asset.php', - strlen( '.js' ) ) );

			// Ensure the file is within the expected directory.
			if ( $file && \strpos( $file, \realpath( $this->asset_path ) ) === 0 && \file_exists( $file ) ) {
				$deps_data = require $file; // nosemgrep.
				$deps      = $deps_data['dependencies'];
				$ver       = $deps_data['version'];
			}
		}

		$deps[] = 'gtmkit';

		// Ask the script registry whether `gtmkit-container` was actually registered earlier in this request rather than re-evaluating the gate predicate, which can disagree with the earlier evaluation if a `gtmkit_container_active` filter callback was added between `register()` and `wp_enqueue_scripts`.
		if ( \wp_script_is( 'gtmkit-container', 'registered' ) ) {
			$deps[] = 'gtmkit-container';
		}

		\wp_enqueue_script( $handle, $this->asset_url . $script, $deps, $ver, $args );
	}

	/**
	 * Get API data
	 *
	 * @param string                $endpoint The API endpoint.
	 * @param string                $transient The transient.
	 * @param array<string, scalar> $extra_args Optional. Additional query args appended to this
	 *                                          request only. Used by callers that need to send
	 *                                          per-endpoint metadata (e.g. plugin version) without
	 *                                          changing the contract for other callers.
	 *
	 * @return array<string, mixed>
	 */
	public function get_data( string $endpoint, string $transient, array $extra_args = [] ): array {
		$data = get_transient( $transient );

		if ( ! WP_DEBUG && $data !== false ) {
			return $data;
		}

		$url = add_query_arg(
			'plugins',
			$this->get_active_plugin_flags(),
			$this->get_api_url( $endpoint )
		);

		if ( ! empty( $extra_args ) ) {
			$url = add_query_arg( $extra_args, $url );
		}

		$response = wp_remote_get( $url );

		if ( is_wp_error( $response ) ) {
			return [];
		}

		$json = wp_remote_retrieve_body( $response );
		$data = json_decode( $json, true );

		if ( json_last_error() !== JSON_ERROR_NONE ) {
			return [];
		}

		set_transient( $transient, $data, 12 * HOUR_IN_SECONDS );

		return $data;
	}

	/**
	 * Map of plugin slugs that the GTM Kit API surfaces use to decide which content is relevant
	 * for the current site.
	 *
	 * Returns booleans for every plugin the API knows about, including the GTM Kit add-ons
	 * themselves so endpoints can target Woo or Premium users without bundling that copy in the
	 * marketplace plugin.
	 *
	 * @return array<string, bool>
	 */
	public function get_active_plugin_flags(): array {
		return [
			'woo'             => \is_plugin_active( 'woocommerce/woocommerce.php' ),
			'cf7'             => \is_plugin_active( 'contact-form-7/wp-contact-form-7.php' ),
			'edd'             => ( \is_plugin_active( 'easy-digital-downloads/easy-digital-downloads.php' ) || \is_plugin_active( 'easy-digital-downloads-pro/easy-digital-downloads.php' ) ),
			'gtm-kit-woo'     => \is_plugin_active( 'gtm-kit-woo/gtm-kit-woo.php' ),
			'gtm-kit-premium' => \is_plugin_active( 'gtm-kit-premium/gtm-kit-premium.php' ),
		];
	}

	/**
	 * Get API Url
	 *
	 * @param string $endpoint The endpoint.
	 *
	 * @return string The API Url.
	 */
	public function get_api_url( string $endpoint ): string {
		return $this->api_host . $this->api_namespace . $endpoint;
	}

	/**
	 * Normalize and hash a string.
	 *
	 * @link https://developers.google.com/google-ads/api/docs/conversions/enhanced-conversions/web#php
	 *
	 * @param string $hash_algorithm The hash algorithm.
	 * @param string $value The string to normalize and hash.
	 * @param bool   $trim_intermediate_spaces Removes leading, trailing, and intermediate spaces.
	 *
	 * @return string The normalized and hashed string.
	 */
	public function normalize_and_hash(
		string $hash_algorithm,
		string $value,
		bool $trim_intermediate_spaces
	): string {
		// Normalizes by first converting all characters to lowercase, then trimming spaces.
		$normalized = strtolower( $value );
		if ( $trim_intermediate_spaces === true ) {
			// Removes leading, trailing, and intermediate spaces.
			$normalized = str_replace( ' ', '', $normalized );
		} else {
			// Removes only leading and trailing spaces.
			$normalized = trim( $normalized );
		}

		if ( $normalized === '' ) {
			return '';
		}

		return hash( $hash_algorithm, strtolower( trim( $normalized ) ) );
	}

	/**
	 * Returns the result of normalizing and hashing an email address. For this use case, Google
	 * Ads requires removal of any '.' characters preceding "gmail.com" or "googlemail.com".
	 *
	 * @param string $hash_algorithm The hash algorithm to use.
	 * @param string $email_address The email address to normalize and hash.
	 * @return string The normalized and hashed email address.
	 */
	public function normalize_and_hash_email_address(
		string $hash_algorithm,
		string $email_address
	): string {
		$normalized_email = strtolower( $email_address );
		$email_parts      = explode( '@', $normalized_email );
		if (
			count( $email_parts ) > 1
			&& preg_match( '/^(gmail|googlemail)\.com\s*/', $email_parts[1] )
		) {
			// Removes any '.' characters from the portion of the email address before the domain
			// if the domain is gmail.com or googlemail.com.
			$email_parts[0]   = str_replace( '.', '', $email_parts[0] );
			$normalized_email = sprintf( '%s@%s', $email_parts[0], $email_parts[1] );
		}
		return $this->normalize_and_hash( $hash_algorithm, $normalized_email, true );
	}

	/**
	 * Get admin page base URL
	 *
	 * @return string
	 */
	public function get_admin_page_url(): string {
		return $this->get_admin_url() . 'admin.php?page=gtmkit_';
	}

	/**
	 * Get the plugin install URL
	 *
	 * @return string
	 */
	public function get_plugin_install_url(): string {
		return $this->get_admin_url() . 'plugin-install.php?tab=search&type=term&s=';
	}

	/**
	 * Get admin url
	 *
	 * @return string
	 */
	private function get_admin_url(): string {
		return is_network_admin() ? network_admin_url() : admin_url();
	}

	/**
	 * Ensure WordPress' admin-side plugin API (`is_plugin_active()`, `get_plugins()`, etc.)
	 * is loaded.
	 *
	 * WordPress loads `wp-admin/includes/plugin.php` automatically on admin requests but not
	 * on the frontend, REST, or AJAX. Any non-admin code path that calls `is_plugin_active()`
	 * or `get_plugins()` must require the file first. Centralised here so the single
	 * `requireOnce.fileNotFound` suppression lives in one place rather than being duplicated
	 * at every call site.
	 *
	 * @return void
	 */
	public static function load_plugin_api(): void {
		if ( ! function_exists( 'is_plugin_active' ) ) {
			// @phpstan-ignore-next-line requireOnce.fileNotFound -- ABSPATH is defined by WordPress at runtime; path is not statically resolvable.
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
	}
}
