<?php

namespace ZenoChatbot\Support;

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

/**
 * Two-phase hook dispatch engine.
 *
 * Phase 1: First-party listeners (internal + Pro) run in priority order.
 * Phase 2: WordPress do_action/apply_filters broadcasts to third-party consumers.
 *
 * Single-value hooks pass the value directly.
 * Multi-value hooks pass an associative array with named keys.
 */
final class HookDispatcher
{
    /** @var array<string, array<int, callable[]>> */
    private array $actions = [];

    /** @var array<string, array<int, callable[]>> */
    private array $filters = [];

    public function on(string $hook, callable $listener, int $priority = 10): void
    {
        $this->actions[$hook][$priority][] = $listener;
    }

    public function filter(string $hook, callable $listener, int $priority = 10): void
    {
        $this->filters[$hook][$priority][] = $listener;
    }

    /**
     * Dispatch an action: first-party listeners first, then WordPress broadcast.
     *
     * @param string $hook
     * @param mixed  $payload Single value or associative array for multi-value hooks
     */
    public function dispatch(string $hook, $payload): void
    {
        foreach ($this->resolve($this->actions, $hook) as $listener) {
            try {
                $listener($payload);
            } catch (\Throwable $e) {
                $this->logError($hook, $e);
            }
        }

        do_action($hook, $payload); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
    }

    /**
     * Apply filters: first-party filters first, then WordPress broadcast.
     *
     * @param string $hook
     * @param mixed  $payload Single value or associative array for multi-value hooks
     * @return mixed The (potentially modified) payload
     */
    public function apply(string $hook, $payload)
    {
        foreach ($this->resolve($this->filters, $hook) as $filter) {
            try {
                $payload = $filter($payload);
            } catch (\Throwable $e) {
                $this->logError($hook, $e);
            }
        }

        return apply_filters($hook, $payload); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
    }

    /**
     * Resolve listeners for a hook sorted by priority.
     *
     * @return callable[]
     */
    private function resolve(array &$registry, string $hook): array
    {
        if (!isset($registry[$hook])) {
            return [];
        }

        $grouped = $registry[$hook];
        ksort($grouped, SORT_NUMERIC);

        $listeners = [];
        foreach ($grouped as $group) {
            foreach ($group as $listener) {
                $listeners[] = $listener;
            }
        }

        return $listeners;
    }

    private function logError(string $hook, \Throwable $e): void
    {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // Plain trigger_error keeps the WP floor at 5.9 (wp_trigger_error needs 6.4).
            // Debug-only path: gated behind WP_DEBUG, never runs in production.
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
            trigger_error(
                esc_html(sprintf(
                    '[ZenoChatbot] %s: Hook listener error on "%s": %s in %s:%d',
                    __METHOD__,
                    $hook,
                    $e->getMessage(),
                    $e->getFile(),
                    $e->getLine()
                )),
                E_USER_NOTICE
            );
        }
    }
}
