<?php

declare(strict_types=1);

namespace Instinctor\Core;

/**
 * AjaxDispatcher — standalone AJAX action registry and dispatcher.
 *
 * On WordPress, admin AJAX is handled natively by wp-admin/admin-ajax.php
 * which fires do_action('wp_ajax_' . $action). Services register handlers
 * with add_action('wp_ajax_*', $callback).
 *
 * On standalone, add_action is a no-op (wp-shims.php). This class provides
 * an equivalent mechanism:
 *   - Services call AjaxDispatcher::register('my_action', [$this, 'handler'])
 *     during their init/constructor.
 *   - AdminRouter routes POST /admin/admin-ajax.php → AjaxDispatcher::dispatch().
 *   - dispatch() reads $_POST['action'], finds the handler, calls it.
 *
 * Handler contract (identical to WP ajax handlers):
 *   - Read params from $_POST / $_FILES
 *   - Call wp_send_json_success($data) or wp_send_json_error($msg) to respond
 *   - Both functions exit — no return value needed
 *
 * @since 3.18.62
 */

if (!defined('ABSPATH')) exit;
class AjaxDispatcher
{
    /** @var array<string, callable> */
    private static array $handlers = [];

    /**
     * Public actions — callable WITHOUT an authenticated admin session.
     * Mirrors WordPress's `wp_ajax_nopriv_*` channel: visitor-facing
     * endpoints (chat send, ajax login, add-to-cart, form submit, etc.)
     * that have to work for logged-out users.
     *
     * AdminRouter consults this set before its `isAuthenticated()` gate
     * so a 403 doesn't kill chat/cart/login traffic on standalone.
     *
     * @var array<string, bool>
     */
    private static array $public = [];

    /**
     * Register an AJAX action handler.
     * Equivalent to: add_action('wp_ajax_' . $action, $callback)
     */
    public static function register(string $action, callable $callback): void
    {
        self::$handlers[$action] = $callback;
    }

    /**
     * Register a PUBLIC AJAX action handler — callable by logged-out
     * visitors. Equivalent to add_action('wp_ajax_nopriv_*', ...) AND
     * add_action('wp_ajax_*', ...) on WordPress. On standalone the
     * handler goes into the same dispatch map but is also flagged as
     * public so AdminRouter skips the auth gate for it.
     */
    public static function registerPublic(string $action, callable $callback): void
    {
        self::$handlers[$action] = $callback;
        self::$public[$action]   = true;
    }

    /**
     * Is the given action callable without authentication?
     * Used by AdminRouter to skip its auth gate for chat / cart /
     * ajax-login / form-submit actions.
     */
    public static function isPublic(string $action): bool
    {
        return !empty(self::$public[$action]);
    }

    /**
     * Dispatch the current request.
     * Called from AdminRouter for POST /admin/admin-ajax.php.
     * Must be called after all services have registered their handlers.
     */
    public static function dispatch(): void
    {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- action routing; handlers verify their own nonces
        // phpcs:disable WordPress.Security.NonceVerification -- action-routing read; the dispatched handler verifies its own Session::check/verify nonce; value sanitize_text_field+wp_unslash'd
        $action = sanitize_text_field(wp_unslash($_POST['action'] ?? ''));
        // phpcs:enable WordPress.Security.NonceVerification

        if ($action === '') {
            wp_send_json_error('Missing action', 400);
        }

        if (!isset(self::$handlers[$action])) {
            // phpcs:disable WordPress.PHP.DevelopmentFunctions -- server-side error capture or migration logging, not debug output
            error_log('[AjaxDispatcher] Unknown action: "' . $action . '", registered actions: ' . implode(',', array_keys(self::$handlers)));
            // phpcs:enable WordPress.PHP.DevelopmentFunctions
            wp_send_json_error('Unknown action: ' . $action, 404);
        }

        call_user_func(self::$handlers[$action]);
    }

    /**
     * Return registered action names (useful for debugging).
     *
     * @return string[]
     */
    public static function registered(): array
    {
        return array_keys(self::$handlers);
    }
}
