<?php

declare(strict_types=1);

namespace App\Http;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Slim\Views\Twig;
use Throwable;

/**
 * UI default error handler. Renders a friendly Twig page; the api side
 * has its JSON equivalent.
 *
 * Slim hands us the raw exception; we log the full trace and decide a
 * status. 4xx pages render with a "you" tone, 5xx with "we", per the
 * SPEC's error-template split. Production env hides the exception
 * message; development shows it.
 */
final class JsonExceptionHandler
{
    public function __construct(
        private readonly Twig $twig,
        private readonly ResponseFactoryInterface $responseFactory,
        private readonly LoggerInterface $logger,
        private readonly bool $isDev,
    ) {
    }

    public function __invoke(
        ServerRequestInterface $request,
        Throwable $exception,
        bool $displayErrorDetails,
        bool $logErrors,
        bool $logErrorDetails,
    ): ResponseInterface {
        $this->logger->error('uncaught exception', [
            'exception' => $exception::class,
            'message' => $exception->getMessage(),
            'file' => $exception->getFile(),
            'line' => $exception->getLine(),
        ]);

        // SEC_REVIEW F73: `Throwable::getCode()` is typed `int` in the
        // interface contract but `PDOException` (and other PDO-derived
        // exceptions) store a SQLSTATE *string* (e.g. `'42S02'`,
        // `'HY000'`) and return it verbatim. The previous loose
        // comparison `>= 400 && < 600` coerced the string to int —
        // `'42S02'` → 42 (no match, OK), but `'400'` → 400 (matches
        // and silently skips the prod message-suppression path
        // applied below). Require a real `int` value before treating
        // it as an HTTP status.
        $status = 500;
        $code = $exception->getCode();
        if (is_int($code) && $code >= 400 && $code < 600) {
            $status = $code;
        }

        $response = $this->responseFactory->createResponse($status);
        try {
            return $this->twig->render($response, 'pages/error.twig', [
                'status' => $status,
                // `$status` is constrained to [400, 599] by the gate
                // above, so client-error == "below 500".
                'is_client_error' => $status < 500,
                'message' => $this->isDev ? $exception->getMessage() : null,
            ]);
        } catch (Throwable) {
            $response->getBody()->write('Server error');

            return $response->withHeader('Content-Type', 'text/plain');
        }
    }
}
