<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Bundle\WebProfilerBundle\Controller;

use Symfony\Bundle\FullStack;
use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
use Symfony\Bundle\WebProfilerBundle\Profiler\TemplateManager;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag;
use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
use Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Profiler\Profiler;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @internal
 */
class ProfilerController
{
    private TemplateManager $templateManager;

    public function __construct(
        private UrlGeneratorInterface $generator,
        private ?Profiler $profiler,
        private Environment $twig,
        private array $templates,
        private ?ContentSecurityPolicyHandler $cspHandler = null,
        private ?string $baseDir = null,
    ) {
    }

    /**
     * Redirects to the last profiles.
     *
     * @throws NotFoundHttpException
     */
    public function homeAction(): RedirectResponse
    {
        $this->denyAccessIfProfilerDisabled();

        return new RedirectResponse($this->generator->generate('_profiler_search_results', ['token' => 'empty', 'limit' => 10]), 302, ['Content-Type' => 'text/html']);
    }

    /**
     * Renders a profiler panel for the given token.
     *
     * @throws NotFoundHttpException
     */
    public function panelAction(Request $request, string $token): Response
    {
        $this->denyAccessIfProfilerDisabled();

        $this->cspHandler?->disableCsp();

        $panel = $request->query->get('panel');
        $page = $request->query->get('page', 'home');
        $profileType = $request->query->get('type', 'request');

        if ('latest' === $token && $latest = current($this->profiler->find(null, null, 1, null, null, null, null, static fn ($profile) => $profileType === $profile['virtual_type']))) {
            $token = $latest['token'];
        }

        if (!$profile = $this->profiler->loadProfile($token)) {
            return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/info.html.twig', ['about' => 'no_token', 'token' => $token, 'request' => $request, 'profile_type' => $profileType]);
        }

        $profileType = $profile->getVirtualType() ?? 'request';

        if (null === $panel) {
            $panel = $profileType;

            foreach ($profile->getCollectors() as $collector) {
                if ($collector instanceof ExceptionDataCollector && $collector->hasException()) {
                    $panel = $collector->getName();

                    break;
                }

                if ($collector instanceof DumpDataCollector && $collector->getDumpsCount() > 0) {
                    $panel = $collector->getName();
                }
            }
        }

        if (!$profile->hasCollector($panel)) {
            throw new NotFoundHttpException(\sprintf('Panel "%s" is not available for token "%s".', $panel, $token));
        }

        return $this->renderWithCspNonces($request, $this->getTemplateManager()->getName($profile, $panel), [
            'token' => $token,
            'profile' => $profile,
            'collector' => $profile->getCollector($panel),
            'panel' => $panel,
            'page' => $page,
            'request' => $request,
            'templates' => $this->getTemplateManager()->getNames($profile),
            'is_ajax' => $request->isXmlHttpRequest(),
            'profiler_markup_version' => 3, // 1 = original profiler, 2 = Symfony 2.8+ profiler, 3 = Symfony 6.2+ profiler
            'profile_type' => $profileType,
        ]);
    }

    /**
     * Renders the Web Debug Toolbar.
     *
     * @throws NotFoundHttpException
     */
    public function toolbarAction(Request $request, ?string $token = null): Response
    {
        if (null === $this->profiler) {
            throw new NotFoundHttpException('The profiler must be enabled.');
        }

        if (!$request->attributes->getBoolean('_stateless') && $request->hasSession()
            && ($session = $request->getSession())->isStarted() && $session->getFlashBag() instanceof AutoExpireFlashBag
        ) {
            // keep current flashes for one more request if using AutoExpireFlashBag
            $session->getFlashBag()->setAll($session->getFlashBag()->peekAll());
        }

        if ('empty' === $token || null === $token) {
            return new Response('', 200, ['Content-Type' => 'text/html']);
        }

        $this->profiler->disable();

        if (!$profile = $this->profiler->loadProfile($token)) {
            return new Response('', 404, ['Content-Type' => 'text/html']);
        }

        $url = null;
        try {
            $url = $this->generator->generate('_profiler', ['token' => $token], UrlGeneratorInterface::ABSOLUTE_URL);
        } catch (\Exception) {
            // the profiler is not enabled
        }

        return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/toolbar.html.twig', [
            'full_stack' => class_exists(FullStack::class),
            'request' => $request,
            'profile' => $profile,
            'templates' => $this->getTemplateManager()->getNames($profile),
            'profiler_url' => $url,
            'token' => $token,
            'profiler_markup_version' => 3, // 1 = original toolbar, 2 = Symfony 2.8+ profiler, 3 = Symfony 6.2+ profiler
        ]);
    }

    /**
     * Renders the Web Debug Toolbar stylesheet.
     *
     * @throws NotFoundHttpException
     */
    public function toolbarStylesheetAction(): Response
    {
        $this->denyAccessIfProfilerDisabled();

        $this->cspHandler?->disableCsp();

        return new Response(
            $this->twig->render('@WebProfiler/Profiler/toolbar.css.twig'),
            200,
            [
                'Content-Type' => 'text/css',
                'Cache-Control' => 'max-age=600, private',
            ],
        );
    }

    /**
     * Renders the profiler search bar.
     *
     * @throws NotFoundHttpException
     */
    public function searchBarAction(Request $request): Response
    {
        $this->denyAccessIfProfilerDisabled();

        $this->cspHandler?->disableCsp();

        $session = null;
        if (!$request->attributes->getBoolean('_stateless') && $request->hasSession()) {
            $session = $request->getSession();
        }

        return new Response(
            $this->twig->render('@WebProfiler/Profiler/search.html.twig', [
                'token' => $request->query->get('token', $session?->get('_profiler_search_token')),
                'ip' => $request->query->get('ip', $session?->get('_profiler_search_ip')),
                'method' => $request->query->get('method', $session?->get('_profiler_search_method')),
                'status_code' => $request->query->get('status_code', $session?->get('_profiler_search_status_code')),
                'url' => $request->query->get('url', $session?->get('_profiler_search_url')),
                'start' => $request->query->get('start', $session?->get('_profiler_search_start')),
                'end' => $request->query->get('end', $session?->get('_profiler_search_end')),
                'limit' => $request->query->get('limit', $session?->get('_profiler_search_limit')),
                'request' => $request,
                'profile_type' => $request->query->get('type', $session?->get('_profiler_search_type', 'request')),
            ]),
            200,
            ['Content-Type' => 'text/html']
        );
    }

    /**
     * Renders the search results.
     *
     * @throws NotFoundHttpException
     */
    public function searchResultsAction(Request $request, string $token): Response
    {
        $this->denyAccessIfProfilerDisabled();

        $this->cspHandler?->disableCsp();

        $profile = $this->profiler->loadProfile($token);

        $ip = $request->query->get('ip');
        $method = $request->query->get('method');
        $statusCode = $request->query->get('status_code');
        $url = $request->query->get('url');
        $start = $request->query->get('start', null);
        $end = $request->query->get('end', null);
        $limit = $request->query->get('limit');
        $profileType = $request->query->get('type', 'request');

        return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/results.html.twig', [
            'request' => $request,
            'token' => $token,
            'profile' => $profile,
            'tokens' => $this->profiler->find($ip, $url, $limit, $method, $start, $end, $statusCode, static fn ($profile) => $profileType === $profile['virtual_type']),
            'ip' => $ip,
            'method' => $method,
            'status_code' => $statusCode,
            'url' => $url,
            'start' => $start,
            'end' => $end,
            'limit' => $limit,
            'panel' => null,
            'profile_type' => $profileType,
        ]);
    }

    /**
     * Narrows the search bar.
     *
     * @throws NotFoundHttpException
     */
    public function searchAction(Request $request): Response
    {
        $this->denyAccessIfProfilerDisabled();

        $ip = $request->query->get('ip');
        $method = $request->query->get('method');
        $statusCode = $request->query->get('status_code');
        $url = $request->query->get('url');
        $start = $request->query->get('start', null);
        $end = $request->query->get('end', null);
        $limit = $request->query->get('limit');
        $token = $request->query->get('token');
        $profileType = $request->query->get('type', 'request');

        if (!$request->attributes->getBoolean('_stateless') && $request->hasSession()) {
            $session = $request->getSession();

            $session->set('_profiler_search_ip', $ip);
            $session->set('_profiler_search_method', $method);
            $session->set('_profiler_search_status_code', $statusCode);
            $session->set('_profiler_search_url', $url);
            $session->set('_profiler_search_start', $start);
            $session->set('_profiler_search_end', $end);
            $session->set('_profiler_search_limit', $limit);
            $session->set('_profiler_search_token', $token);
            $session->set('_profiler_search_type', $profileType);
        }

        if ($token) {
            return new RedirectResponse($this->generator->generate('_profiler', ['token' => $token]), 302, ['Content-Type' => 'text/html']);
        }

        $tokens = $this->profiler->find($ip, $url, $limit, $method, $start, $end, $statusCode, static fn ($profile) => $profileType === $profile['virtual_type']);

        return new RedirectResponse($this->generator->generate('_profiler_search_results', [
            'token' => $tokens ? $tokens[0]['token'] : 'empty',
            'ip' => $ip,
            'method' => $method,
            'status_code' => $statusCode,
            'url' => $url,
            'start' => $start,
            'end' => $end,
            'limit' => $limit,
            'type' => $profileType,
        ]), 302, ['Content-Type' => 'text/html']);
    }

    /**
     * Displays the PHP info.
     *
     * @throws NotFoundHttpException
     */
    public function phpinfoAction(): Response
    {
        $this->denyAccessIfProfilerDisabled();

        $this->cspHandler?->disableCsp();

        ob_start();
        phpinfo();
        $phpinfo = ob_get_clean();

        return new Response($phpinfo, 200, ['Content-Type' => 'text/html']);
    }

    /**
     * Displays the Xdebug info.
     *
     * @throws NotFoundHttpException
     */
    public function xdebugAction(): Response
    {
        $this->denyAccessIfProfilerDisabled();

        if (!\function_exists('xdebug_info')) {
            throw new NotFoundHttpException('Xdebug must be installed in version 3.');
        }

        $this->cspHandler?->disableCsp();

        ob_start();
        xdebug_info();
        $xdebugInfo = ob_get_clean();

        return new Response($xdebugInfo, 200, ['Content-Type' => 'text/html']);
    }

    /**
     * Returns the custom web fonts used in the profiler.
     *
     * @throws NotFoundHttpException
     */
    public function fontAction(string $fontName): Response
    {
        $this->denyAccessIfProfilerDisabled();
        if ('JetBrainsMono' !== $fontName) {
            throw new NotFoundHttpException(\sprintf('Font file "%s.woff2" not found.', $fontName));
        }

        $fontFile = \dirname(__DIR__).'/Resources/fonts/'.$fontName.'.woff2';
        if (!is_file($fontFile) || !is_readable($fontFile)) {
            throw new NotFoundHttpException(\sprintf('Cannot read font file "%s".', $fontFile));
        }

        $this->profiler?->disable();

        return new BinaryFileResponse($fontFile, 200, ['Content-Type' => 'font/woff2']);
    }

    /**
     * Displays the source of a file.
     *
     * @throws NotFoundHttpException
     */
    public function openAction(Request $request): Response
    {
        if (null === $this->baseDir) {
            throw new NotFoundHttpException('The base dir should be set.');
        }

        $this->profiler?->disable();

        $file = $request->query->get('file');
        $line = $request->query->get('line');

        $filename = $this->baseDir.\DIRECTORY_SEPARATOR.$file;

        if (preg_match("'(^|[/\\\\])\.'", $file) || !is_readable($filename)) {
            throw new NotFoundHttpException(\sprintf('The file "%s" cannot be opened.', $file));
        }

        return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/open.html.twig', [
            'file_info' => new \SplFileInfo($filename),
            'file' => $file,
            'line' => $line,
        ]);
    }

    protected function getTemplateManager(): TemplateManager
    {
        return $this->templateManager ??= new TemplateManager($this->profiler, $this->twig, $this->templates);
    }

    /**
     * @throws NotFoundHttpException
     */
    private function denyAccessIfProfilerDisabled(): void
    {
        if (null === $this->profiler) {
            throw new NotFoundHttpException('The profiler must be enabled.');
        }

        $this->profiler->disable();
    }

    private function renderWithCspNonces(Request $request, string $template, array $variables, int $code = 200, array $headers = ['Content-Type' => 'text/html']): Response
    {
        $response = new Response('', $code, $headers);

        $nonces = $this->cspHandler ? $this->cspHandler->getNonces($request, $response) : [];

        $variables['csp_script_nonce'] = $nonces['csp_script_nonce'] ?? null;
        $variables['csp_style_nonce'] = $nonces['csp_style_nonce'] ?? null;

        $response->setContent($this->twig->render($template, $variables));

        return $response;
    }
}
