<?php

declare(strict_types=1);

namespace BotGuard\Core;

use BotGuard\Core\Cache\CacheInterface;
use BotGuard\Core\Cache\FileCache;
use BotGuard\Core\Exceptions\HttpException;
use BotGuard\Middleware\AdminMiddleware;
use BotGuard\Middleware\ApiKeyAuthMiddleware;
use BotGuard\Middleware\AuthMiddleware;
use BotGuard\Middleware\CsrfMiddleware;
use BotGuard\Middleware\MiddlewareInterface;
use BotGuard\Middleware\RateLimitMiddleware;
use BotGuard\Middleware\SecurityHeadersMiddleware;
use BotGuard\Middleware\SetupMiddleware;
use Closure;
use Throwable;

/**
 * Application kernel.
 *
 * Wires the container, registers core service bindings, loads route files, runs
 * the request through the global + route middleware pipeline to a controller,
 * and converts any exception into a proper HTTP response. This is the single
 * entry point invoked by public/index.php.
 */
final class Kernel
{
    private Container $container;

    private Router $router;

    /** Middleware applied to every request, in order. */
    private array $globalMiddleware = [
        SecurityHeadersMiddleware::class,
        SetupMiddleware::class,
    ];

    /** Named middleware referenced by routes. */
    private array $routeMiddleware = [
        'csrf'      => CsrfMiddleware::class,
        'auth'      => AuthMiddleware::class,
        'admin'     => AdminMiddleware::class,
        'throttle'  => RateLimitMiddleware::class,
        'apikey'    => ApiKeyAuthMiddleware::class,
    ];

    public function __construct(private string $basePath)
    {
        Config::boot($basePath);

        $this->container = new Container();
        Container::setInstance($this->container);
        $this->router = new Router();

        $this->registerBindings();
        $this->loadRoutes();
    }

    /**
     * Handle an incoming request and return the response.
     */
    public function handle(Request $request): Response
    {
        $this->container->instance(Request::class, $request);

        try {
            $match = $this->router->match($request);
            $request->setRouteParams($match['params']);

            $middleware = array_merge($this->globalMiddleware, $this->resolveRouteMiddleware($match['middleware']));

            $pipeline = $this->buildPipeline(
                $middleware,
                fn (Request $req): Response => $this->runController($match['handler'], $req)
            );

            return $pipeline($request);
        } catch (HttpException $e) {
            return $this->renderError($request, $e->getStatusCode(), $e->getMessage());
        } catch (Throwable $e) {
            $this->container->make(Logger::class)->error('Unhandled exception', [
                'message' => $e->getMessage(),
                'file'    => $e->getFile() . ':' . $e->getLine(),
                'trace'   => explode("\n", $e->getTraceAsString()),
            ]);

            $debug = (bool) config('app.debug', false);
            $message = $debug ? $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine() : 'Server Error';

            return $this->renderError($request, 500, $message);
        }
    }

    public function container(): Container
    {
        return $this->container;
    }

    public function router(): Router
    {
        return $this->router;
    }

    /**
     * Register framework service bindings in the container.
     */
    private function registerBindings(): void
    {
        $this->container->instance(Router::class, $this->router);

        $this->container->singleton(Database::class, static fn (): Database => Database::fromConfig());

        $this->container->singleton(Logger::class, fn (): Logger => new Logger($this->basePath . '/storage/logs'));

        $this->container->singleton(CacheInterface::class, fn (): CacheInterface => new FileCache($this->basePath . '/storage/cache'));

        $this->container->singleton(Session::class, static function (): Session {
            $session = new Session();
            $session->start();

            return $session;
        });

        $this->container->singleton(Csrf::class, fn (Container $c): Csrf => new Csrf($c->make(Session::class)));

        $this->container->singleton(Auth::class, fn (Container $c): Auth => new Auth(
            $c->make(Session::class),
            $c->make(Database::class),
        ));

        $this->container->singleton(View::class, fn (): View => new View($this->basePath . '/app/Views'));
    }

    /**
     * Load route definition files. Each returns a closure receiving the Router.
     */
    private function loadRoutes(): void
    {
        foreach (['install', 'web', 'api'] as $file) {
            $path = $this->basePath . '/routes/' . $file . '.php';
            if (is_file($path)) {
                (require $path)($this->router);
            }
        }
    }

    /**
     * Map route-middleware aliases to concrete middleware class names.
     *
     * @param string[] $aliases
     * @return string[]
     */
    private function resolveRouteMiddleware(array $aliases): array
    {
        $resolved = [];

        foreach ($aliases as $alias) {
            $resolved[] = $this->routeMiddleware[$alias] ?? $alias;
        }

        return $resolved;
    }

    /**
     * Compose the middleware pipeline as nested closures around the destination.
     *
     * @param string[] $middleware
     */
    private function buildPipeline(array $middleware, Closure $destination): Closure
    {
        return array_reduce(
            array_reverse($middleware),
            function (Closure $next, string $middlewareClass): Closure {
                return function (Request $request) use ($middlewareClass, $next): Response {
                    /** @var MiddlewareInterface $instance */
                    $instance = $this->container->make($middlewareClass);

                    return $instance->handle($request, $next);
                };
            },
            $destination
        );
    }

    /**
     * Resolve and invoke the matched route handler.
     */
    private function runController(mixed $handler, Request $request): Response
    {
        if ($handler instanceof Closure) {
            return $this->normalize($handler($request));
        }

        // "Controller@method" string form.
        [$class, $method] = explode('@', (string) $handler, 2);
        $controller = $this->container->make($class);

        $result = $controller->{$method}($request);

        return $this->normalize($result);
    }

    /**
     * Coerce a controller return value into a Response.
     */
    private function normalize(mixed $result): Response
    {
        if ($result instanceof Response) {
            return $result;
        }

        if (is_array($result)) {
            return Response::json($result);
        }

        return new Response((string) $result);
    }

    /**
     * Render an error either as JSON or a themed HTML error page.
     */
    private function renderError(Request $request, int $status, string $message): Response
    {
        if ($request->wantsJson()) {
            return Response::json(['error' => $message ?: 'Error', 'status' => $status], $status);
        }

        try {
            $view = $this->container->make(View::class);
            $html = $view->render('errors.generic', ['status' => $status, 'message' => $message]);

            return (new Response($html, $status))->withHeader('Content-Type', 'text/html; charset=utf-8');
        } catch (Throwable) {
            return new Response("<h1>{$status}</h1><p>" . e($message) . '</p>', $status);
        }
    }
}
