<?php
declare(strict_types=1);

namespace App\Support;

use App\Support\FileLogger;
use Throwable;

final class Hooks
{
    /** @var array<string, array<int, list<callable>>> */
    private static array $listeners = [];

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

    public static function on(string $event, callable $listener, int $priority = 10): void
    {
        self::$listeners[$event][$priority][] = $listener;
    }

    /** @return array{listeners:array<string,int>,total:int} */
    public static function diagnostics(): array
    {
        $listeners = [];
        $total = 0;
        foreach (self::$listeners as $name => $byPriority) {
            $count = 0;
            foreach ($byPriority as $callbacks) $count += count($callbacks);
            $listeners[$name] = $count;
            $total += $count;
        }
        ksort($listeners);
        return ['listeners' => $listeners, 'total' => $total];
    }

    /**
     * Actions: side effects, no return value.
     *
     * Every listener is wrapped in a try/catch block so an exception thrown
     * by a plugin or hook never halts execution or breaks the site.
     *
     * @param array<string, mixed> $payload
     */
    public static function emit(string $event, array $payload = []): void
    {
        $byPriority = self::$listeners[$event] ?? [];
        if ($byPriority === []) {
            return;
        }

        ksort($byPriority, SORT_NUMERIC);

        foreach ($byPriority as $listeners) {
            foreach ($listeners as $listener) {
                try {
                    $listener($payload);
                } catch (Throwable $throwable) {
                    try {
                        (new FileLogger())->error(
                            "Hook action [{$event}] listener failed: " . $throwable->getMessage(),
                            $throwable
                        );
                    } catch (Throwable) {
                        // Logger failure must never bubble out of a hook
                    }
                }
            }
        }
    }

    /**
     * Filters: each listener receives the value and returns it, possibly changed.
     *
     * If a filter listener throws an exception, the error is logged and the
     * unfiltered/current value is preserved and passed to the next listener.
     *
     * @param array<string, mixed> $context
     */
    public static function filter(string $name, mixed $value, array $context = []): mixed
    {
        $byPriority = self::$listeners[$name] ?? [];
        if ($byPriority === []) {
            return $value;
        }

        ksort($byPriority, SORT_NUMERIC);

        $current = $value;
        foreach ($byPriority as $listeners) {
            foreach ($listeners as $listener) {
                try {
                    $current = $listener($current, $context);
                } catch (Throwable $throwable) {
                    try {
                        (new FileLogger())->error(
                            "Hook filter [{$name}] listener failed: " . $throwable->getMessage(),
                            $throwable
                        );
                    } catch (Throwable) {
                        // Logger failure must never bubble out of a hook
                    }
                }
            }
        }

        return $current;
    }
}
