<?php
/**
 * Hook Tracker
 *
 * Tracks WordPress hook registrations per module for performance monitoring.
 *
 * @package WP_PowerSuite\Core
 * @since 1.0.0
 */

namespace WP_PowerSuite\Core;

/**
 * Hook Tracker Class
 *
 * Counts hook registrations per module by snapshotting `$wp_filter` before and
 * after module init. Active when {@see PerformanceBudgetConfig::shouldTrackModuleHooks()}
 * is true (default: every request, once per enabled module).
 *
 * @since 1.0.0
 */
class HookTracker {

	/**
	 * Hook Tracker Instance
	 *
	 * Singleton instance of the HookTracker class.
	 *
	 * @var HookTracker|null
	 * @since 1.0.0
	 */
	private static ?HookTracker $instance = null;

	/**
	 * Hook Snapshots
	 *
	 * Snapshots of total hook count before module initialization.
	 *
	 * @var array<string, int> Module ID => hook count before init
	 * @since 1.0.0
	 */
	private array $hook_snapshots = [];

	/**
	 * Get Hook Tracker Instance
	 *
	 * Returns the singleton instance of the HookTracker class.
	 *
	 * @since 1.0.0
	 * @return HookTracker HookTracker instance.
	 */
	public static function getInstance(): HookTracker {
		if ( self::$instance === null ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor
	 *
	 * Private constructor to enforce singleton pattern.
	 *
	 * @since 1.0.0
	 */
	private function __construct() {
		// Singleton.
	}

	/**
	 * Prevent Cloning
	 *
	 * Prevents cloning of the singleton instance.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	private function __clone() {
		// Prevent cloning
	}

	/**
	 * Prevent Unserialization
	 *
	 * Prevents unserialization of the singleton instance.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function __wakeup(): void {
		throw new \Exception( 'Cannot unserialize singleton' );
	}

	/**
	 * Whether hook delta tracking is active for this request.
	 */
	public function isTrackingEnabled(): bool {
		return PerformanceBudgetConfig::shouldTrackModuleHooks();
	}

	/**
	 * Take Hook Snapshot
	 *
	 * Records total hook count before module init.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID.
	 * @return void
	 */
	public function takeSnapshot( string $module_id ): void {
		if ( ! $this->isTrackingEnabled() ) {
			return;
		}

		$this->hook_snapshots[ $module_id ] = $this->getHookCount();
	}

	/**
	 * Get Hook Count Delta
	 *
	 * Calculates hooks registered during module init.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID.
	 * @return int Number of hooks registered.
	 */
	public function getHookCountDelta( string $module_id ): int {
		if ( ! $this->isTrackingEnabled() ) {
			return 0;
		}

		$before = $this->hook_snapshots[ $module_id ] ?? 0;
		$after  = $this->getHookCount();

		return max( 0, $after - $before );
	}

	/**
	 * Get Current Hook Count
	 *
	 * Counts total number of hooks currently registered in WordPress.
	 *
	 * @since 1.0.0
	 * @return int Total hook count.
	 */
	private function getHookCount(): int {
		global $wp_filter;

		if ( ! is_array( $wp_filter ) ) {
			return 0;
		}

		$count = 0;
		foreach ( $wp_filter as $hook_callbacks ) {
			if ( isset( $hook_callbacks->callbacks ) && is_array( $hook_callbacks->callbacks ) ) {
				foreach ( $hook_callbacks->callbacks as $callbacks ) {
					if ( is_array( $callbacks ) ) {
						$count += count( $callbacks );
					}
				}
			}
		}

		return $count;
	}

	/**
	 * Count Module Hooks (namespace scan)
	 *
	 * Optional helper that estimates hooks by module namespace. Dev-only utility;
	 * runtime budgets use {@see getHookCountDelta()} instead.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID.
	 * @param string $module_namespace Module namespace.
	 * @return int Estimated hook count.
	 */
	public function countModuleHooks( string $module_id, string $module_namespace ): int {
		if ( ! PerformanceBudgetConfig::isDetailedMetricsEnabled() ) {
			return 0;
		}

		global $wp_filter;

		if ( ! is_array( $wp_filter ) ) {
			return 0;
		}

		$count             = 0;
		$namespace_pattern = str_replace( '\\', '\\\\', $module_namespace );

		foreach ( $wp_filter as $hook_callbacks ) {
			if ( isset( $hook_callbacks->callbacks ) && is_array( $hook_callbacks->callbacks ) ) {
				foreach ( $hook_callbacks->callbacks as $callbacks ) {
					if ( ! is_array( $callbacks ) ) {
						continue;
					}
					foreach ( $callbacks as $callback ) {
						if ( $this->isModuleCallback( $callback, $namespace_pattern ) ) {
							++$count;
						}
					}
				}
			}
		}

		return $count;
	}

	/**
	 * Check if Callback is from Module
	 *
	 * Checks if a callback function belongs to a module namespace.
	 *
	 * @since 1.0.0
	 * @param mixed  $callback Callback to check.
	 * @param string $namespace_pattern Module namespace pattern.
	 * @return bool True if callback is from module, false otherwise.
	 */
	private function isModuleCallback( mixed $callback, string $namespace_pattern ): bool {
		if ( is_array( $callback ) && isset( $callback['function'] ) ) {
			$callback = $callback['function'];
		}

		if ( is_array( $callback ) && count( $callback ) === 2 ) {
			if ( is_object( $callback[0] ) ) {
				$class = get_class( $callback[0] );
				return strpos( $class, $namespace_pattern ) === 0;
			}
			if ( is_string( $callback[0] ) ) {
				return strpos( $callback[0], $namespace_pattern ) === 0;
			}
		}

		return is_string( $callback ) && strpos( $callback, $namespace_pattern ) === 0;
	}
}
