<?php
/**
 * Hook registrar.
 *
 * Centralizes calls to WordPress's add_action() and add_filter() so that
 * hook wiring is expressed as data rather than scattered imperative calls.
 *
 * A HookRegistrar instance is created once by the Plugin bootstrap and
 * passed to each registered service provider. Providers call the
 * addAction() / addFilter() methods on it; the registrar forwards the
 * registration to WordPress and retains a record so that tests and
 * diagnostics can introspect what has been wired.
 *
 * This class deliberately contains no business logic and no knowledge
 * of any specific plugin subsystem. It is a thin, testable seam over
 * WordPress's hook API.
 *
 * @package Affilun\Bootstrap
 */

declare(strict_types=1);

namespace Affilun\Bootstrap;

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

/**
 * Thin wrapper over add_action() / add_filter() with introspection support.
 *
 * @phpstan-type HookRecord array{
 *     type: 'action'|'filter',
 *     hook: string,
 *     callback: callable,
 *     priority: int,
 *     accepted_args: int,
 * }
 */
final class HookRegistrar {

	/**
	 * Ordered list of every hook that has been registered through this
	 * registrar. Used by tests and the diagnostics surface.
	 *
	 * @var list<HookRecord>
	 */
	private array $registered = array();

	/**
	 * Register an action callback.
	 *
	 * Forwards to WordPress's add_action() when it is available (the
	 * normal runtime path) and records the registration for introspection.
	 * When add_action() is not available (pure-unit test context without
	 * WordPress loaded), the registration is still recorded so tests can
	 * assert against it; no error is raised.
	 *
	 * @param string   $hook          Action hook name.
	 * @param callable $callback      Callback to invoke.
	 * @param int      $priority      Hook priority. Default 10.
	 * @param int      $accepted_args Number of arguments the callback accepts. Default 1.
	 */
	public function addAction(
		string $hook,
		callable $callback,
		int $priority = 10,
		int $accepted_args = 1
	): void {
		$this->record( 'action', $hook, $callback, $priority, $accepted_args );

		if ( function_exists( 'add_action' ) ) {
			add_action( $hook, $callback, $priority, $accepted_args );
		}
	}

	/**
	 * Register a filter callback.
	 *
	 * See {@see HookRegistrar::addAction()} for semantics; the only
	 * difference is that this delegates to add_filter().
	 *
	 * @param string   $hook          Filter hook name.
	 * @param callable $callback      Callback to invoke.
	 * @param int      $priority      Hook priority. Default 10.
	 * @param int      $accepted_args Number of arguments the callback accepts. Default 1.
	 */
	public function addFilter(
		string $hook,
		callable $callback,
		int $priority = 10,
		int $accepted_args = 1
	): void {
		$this->record( 'filter', $hook, $callback, $priority, $accepted_args );

		if ( function_exists( 'add_filter' ) ) {
			add_filter( $hook, $callback, $priority, $accepted_args );
		}
	}

	/**
	 * Return every hook registration made through this registrar, in order.
	 *
	 * @return list<HookRecord>
	 */
	public function registered(): array {
		return $this->registered;
	}

	/**
	 * Return only the registrations for the given hook name.
	 *
	 * Useful for targeted assertions in tests (e.g. "the plugin must
	 * register exactly one callback on plugins_loaded") without having
	 * to scan the full list.
	 *
	 * @param string $hook Hook name to filter by.
	 *
	 * @return list<HookRecord>
	 */
	public function registeredFor( string $hook ): array {
		return array_values(
			array_filter(
				$this->registered,
				static fn ( array $record ): bool => $record['hook'] === $hook
			)
		);
	}

	/**
	 * Report whether any registration has been made for the given hook.
	 *
	 * @param string $hook Hook name to check.
	 */
	public function hasHook( string $hook ): bool {
		foreach ( $this->registered as $record ) {
			if ( $record['hook'] === $hook ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Total number of registrations made through this registrar.
	 */
	public function count(): int {
		return count( $this->registered );
	}

	/**
	 * Record a single hook registration in the internal log.
	 *
	 * @param 'action'|'filter' $type          Hook type.
	 * @param string            $hook          Hook name.
	 * @param callable          $callback      Callback.
	 * @param int               $priority      Priority.
	 * @param int               $accepted_args Accepted argument count.
	 */
	private function record(
		string $type,
		string $hook,
		callable $callback,
		int $priority,
		int $accepted_args
	): void {
		$this->registered[] = array(
			'type'          => $type,
			'hook'          => $hook,
			'callback'      => $callback,
			'priority'      => $priority,
			'accepted_args' => $accepted_args,
		);
	}
}
