<?php

namespace App\Core;

/**
 * ITSWEBER CMS – Minimal Hook Registry (Phase 17)
 *
 * Provides a simple action/filter hook system for plugins.
 * Hooks are registered in memory and executed synchronously.
 *
 * API:
 *   Hooks::add('hook.name', callable, priority = 10)  – register a listener
 *   Hooks::run('hook.name', ...$args)                 – fire an action hook
 *   Hooks::filter('hook.name', $value, ...$args)      – fire a filter hook (returns modified value)
 *   Hooks::has('hook.name')                           – check if listeners exist
 *
 * Global wrapper functions are defined in app/Helpers/functions.php:
 *   add_hook(), run_hook(), apply_hook()
 */
class Hooks
{
    /** @var array<string, array<int, callable[]>> */
    private static array $hooks = [];

    /**
     * Register a callback for a named hook.
     *
     * @param string   $name     Hook name, e.g. 'cms.boot', 'page.render'
     * @param callable $callback Any PHP callable
     * @param int      $priority Lower runs first. Default: 10
     */
    public static function add(string $name, callable $callback, int $priority = 10): void
    {
        self::$hooks[$name][$priority][] = $callback;
    }

    /**
     * Fire an action hook – all registered callbacks are called in priority order.
     * Return values are ignored.
     *
     * @param string $name   Hook name
     * @param mixed  ...$args Arguments forwarded to each callback
     */
    public static function run(string $name, mixed ...$args): void
    {
        if (empty(self::$hooks[$name])) {
            return;
        }

        ksort(self::$hooks[$name]);

        foreach (self::$hooks[$name] as $callbacks) {
            foreach ($callbacks as $callback) {
                try {
                    $callback(...$args);
                } catch (\Throwable $e) {
                    // Isolate callback errors – log but do not crash the CMS
                    error_log('[Hooks::run] Error in hook "' . $name . '": ' . $e->getMessage());
                }
            }
        }
    }

    /**
     * Fire a filter hook – callbacks can modify $value and must return it.
     * If no callbacks are registered, the original value is returned unchanged.
     *
     * @param string $name   Hook name
     * @param mixed  $value  The value to filter
     * @param mixed  ...$args Additional read-only context arguments
     * @return mixed The (possibly modified) value
     */
    public static function filter(string $name, mixed $value, mixed ...$args): mixed
    {
        if (empty(self::$hooks[$name])) {
            return $value;
        }

        ksort(self::$hooks[$name]);

        foreach (self::$hooks[$name] as $callbacks) {
            foreach ($callbacks as $callback) {
                try {
                    $value = $callback($value, ...$args);
                } catch (\Throwable $e) {
                    error_log('[Hooks::filter] Error in hook "' . $name . '": ' . $e->getMessage());
                }
            }
        }

        return $value;
    }

    /**
     * Check whether any callbacks are registered for a hook.
     */
    public static function has(string $name): bool
    {
        return !empty(self::$hooks[$name]);
    }

    /**
     * Remove all registered callbacks for a hook (useful for testing).
     */
    public static function clear(string $name): void
    {
        unset(self::$hooks[$name]);
    }

    /**
     * Remove all registered hooks entirely (useful for testing).
     */
    public static function clearAll(): void
    {
        self::$hooks = [];
    }
}
