<?php

namespace App\Core;

/**
 * MiddlewarePipeline — Ordered Middleware Execution Stack
 *
 * Implements the "onion" (decorator) pattern for middleware execution.
 * Middleware class names are wrapped around a destination callable from
 * outermost to innermost, forming a nested execution chain:
 *
 *   Middleware[0] → Middleware[1] → Middleware[n] → destination (controller action)
 *
 * Each middleware layer receives the Request and a $next callable.
 * Calling $next($request) continues the pipeline to the next layer.
 * Not calling $next terminates execution early (e.g. redirect, 403).
 *
 * Middleware contract (CORE-001):
 *   Each middleware class MUST implement MiddlewareInterface, which declares:
 *     public function handle(Request $request, callable $next): void
 *   A convention-only method_exists() check was used in INIT-009 (Sprint 0).
 *   CORE-001 adds a formal instanceof MiddlewareInterface verification that
 *   logs a warning when a class omits the interface declaration while still
 *   implementing handle() — preserving backwards compatibility.
 *   Middleware classes are resolved via the PSR-4 Autoloader (INIT-003).
 *   No DI container. No Reflection.
 *
 * Usage — called by Router::dispatch() after a route is matched:
 *   $pipeline = new MiddlewarePipeline($route['middleware']);
 *   $pipeline->run($request, function (Request $req) use ($action): void {
 *       $this->invoke($action, $req);
 *   });
 *
 * @package Inveetaire
 * @ref     DBP v1.1 Ch.4, SAD v1.1 Ch.5, INIT-009, CORE-001
 */
class MiddlewarePipeline
{
    /**
     * Ordered list of fully-qualified middleware class names.
     * Index 0 is the outermost layer (executes first).
     *
     * Example:
     *   ['App\Middleware\AuthMiddleware', 'App\Middleware\RoleMiddleware']
     *
     * @var string[]
     */
    private array $middlewareClasses;

    /**
     * @param string[] $middlewareClasses Ordered middleware class names (FQN).
     */
    public function __construct(array $middlewareClasses)
    {
        $this->middlewareClasses = $middlewareClasses;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PUBLIC
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Execute the middleware stack, ending with the destination (controller action).
     *
     * If $middlewareClasses is empty, $destination is called directly.
     *
     * @param Request  $request     The incoming HTTP request.
     * @param callable $destination The controller action, invoked after all middleware.
     *                              Signature: function(Request $request): void
     * @return void
     */
    public function run(Request $request, callable $destination): void
    {
        $pipeline = $this->build($destination);
        $pipeline($request);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PRIVATE
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Build the nested callable chain from the middleware list.
     *
     * The list is processed in reverse so that index-0 middleware becomes
     * the outermost callable — the first one executed when the chain is called.
     *
     * Build trace (3 middleware + destination):
     *   start:  $next = destination
     *   step 1: $next = wrap(middleware[2], destination)
     *   step 2: $next = wrap(middleware[1], wrap(middleware[2], destination))
     *   step 3: $next = wrap(middleware[0], wrap(middleware[1], wrap(middleware[2], destination)))
     *   call:   $next($request) → middleware[0] runs first
     *
     * @param callable $destination Final callable (controller action).
     * @return callable             Outermost layer — ready to call with $request.
     */
    private function build(callable $destination): callable
    {
        $next = $destination;

        foreach (array_reverse($this->middlewareClasses) as $class) {
            $next = $this->wrap($class, $next);
        }

        return $next;
    }

    /**
     * Create a single middleware layer as a callable closure.
     *
     * The closure:
     *   1. Verifies the class exists (triggers PSR-4 autoload).
     *   2. Verifies the class implements MiddlewareInterface (CORE-001).
     *      A warning is logged when the interface is absent but handle()
     *      exists — backwards-compatible, never blocks execution.
     *   3. Verifies the class has a handle() method.
     *   4. Instantiates the class with new $class().
     *   5. Calls handle(Request, callable $next).
     *
     * On failure (class not found or handle() missing):
     *   Logs the error and delegates to ErrorHandler — never a raw PHP fatal.
     *
     * @param string   $class Fully-qualified middleware class name.
     * @param callable $next  The next layer in the chain.
     * @return callable       A callable that runs this middleware then $next.
     */
    private function wrap(string $class, callable $next): callable
    {
        return function (Request $request) use ($class, $next): void {

            // class_exists() triggers the PSR-4 Autoloader — no Reflection used.
            if (!class_exists($class)) {
                Logger::error('Middleware class not found', ['class' => $class]);
                ErrorHandler::handleException(
                    new \RuntimeException("Middleware class '{$class}' could not be loaded.")
                );
                return;
            }

            // CORE-001: formal interface compliance check (additive — warning only).
            // Middleware that implements handle() without declaring MiddlewareInterface
            // continues to work but logs a deprecation-style warning. This preserves
            // backwards compatibility while enforcing the interface going forward.
            if (!($class instanceof MiddlewareInterface) && !is_subclass_of($class, MiddlewareInterface::class)) {
                Logger::warning(
                    'Middleware does not implement MiddlewareInterface',
                    ['class' => $class]
                );
            }

            if (!method_exists($class, 'handle')) {
                Logger::error('Middleware missing handle() method', ['class' => $class]);
                ErrorHandler::handleException(
                    new \RuntimeException(
                        "Middleware '{$class}' must implement: public function handle(Request \$request, callable \$next): void"
                    )
                );
                return;
            }

            (new $class())->handle($request, $next);
        };
    }
}
