<?php

namespace WordpressModels\DependencyInjection\HookAttributes;

use Symfony\Component\DependencyInjection\ContainerInterface;
use WordpressModels\DependencyInjection\HookAttributes\Attributes\Action;
use WordpressModels\DependencyInjection\Initializer\Attributes\ForceInitialize;

/**
 * Registry for hooks.
 *
 * At compile time, the hooks added to this registry. The {@see ContainerHookRegistry::registerHooks()} method
 * is called on 'plugins_loaded' to register the hooks.
 */
#[ForceInitialize]
class ContainerHookRegistry
{

    /**
     * @var array{
     *     type: string,
     *     hook: string,
     *     callback: string,
     *     priority: int,
     *     accepted_args: int,
     *     when: string|null,
     *     admin: bool,
     *     ajax: bool,
     * }[]
     */
    private array $hooks = [];

    public function __construct(private ContainerInterface $container)
    {
    }

    public function registerHooks()
    {
        foreach ($this->hooks as $action) {
            // skip if ajax is false and we are doing ajax
            // we resolve this after the service container is built, as we don't separate the container
            // for ajax context, it's just the admin container
            if (function_exists('wp_doing_ajax') && wp_doing_ajax() && isset($action['ajax']) && !$action['ajax']) {
                continue;
            }

            if (isset($action['when']) && $action['when']) {
                add_action($action['when'], function () use ($action) {
                    $this->registerHook($action);
                });
                continue;
            }

            $this->registerHook($action);
        }
    }

    #[Action('before_reset_container', 10)]
    public function deregisterHooks()
    {
        foreach ($this->hooks as $action) {
            call_user_func('remove_' . $action['type'],
                $action['hook'],
                $action['callback'],
                $action['priority']);
        }
    }

    public function addHook(string   $id,
                            string   $type,
                            callable $callback,
                            string   $hook,
                            int      $priority = 10,
                            int      $accepted_args = 1,
                            ?string  $when = null,
                            bool     $ajax = false,
                            bool     $admin = false,
                            array    $injectServices = []
    )
    {
        $this->hooks[] = [
            'type' => $type,
            'hook' => $hook,
            'callback' => $callback,
            'priority' => $priority,
            'accepted_args' => $accepted_args,
            'when' => $when,
            'ajax' => $ajax,
            'admin' => $admin,
            'injectServices' => $injectServices,
        ];
    }

    public function addAjaxHook(string   $action,
                                bool     $private,
                                bool     $public,
                                int      $priority,
                                ?string  $nonceId,
                                string   $nonceKey,
                                callable $handler)
    {
        if ($private) {
            $this->hooks[] = [
                'type' => 'action',
                'hook' => "wp_ajax_$action",
                'callback' => new AjaxActionWrapper($handler, $nonceId, $nonceKey),
                'priority' => $priority,
                'accepted_args' => 0
            ];
        }

        if ($public) {
            $this->hooks[] = [
                'type' => 'action',
                'hook' => "wp_ajax_nopriv_$action",
                'callback' => new AjaxActionWrapper($handler, $nonceId, $nonceKey),
                'priority' => $priority,
                'accepted_args' => 0
            ];
        }
    }

    /**
     * @param array $action
     * @return void
     */
    public function registerHook(array $action): void
    {
        $callback = ($action['injectServices'] ?? false)
            ? $this->wrapCallback($action['callback'], $action['injectServices'])
            : $action['callback'];

        call_user_func('add_' . $action['type'],
            $action['hook'],
            $callback,
            $action['priority'],
            $action['accepted_args']);
    }


    /**
     * Create a wrapper for the callback that injects the services.
     *
     * @param callable $callback -- original callback
     * @param array $injectServices -- in-order services to inject
     * @return \Closure
     */
    private function wrapCallback(callable $callback, array $injectServices)
    {
        return function (...$args) use ($callback, $injectServices) {
            $services = array_map(
                fn($serviceId) => $this->container->get($serviceId),
                $injectServices
            );
            $args = array_merge($args, $services);
            return call_user_func($callback, ...$args);
        };
    }

}