<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
declare(strict_types=1);

namespace Magento\Framework\App;

use Magento\Framework\App\Response\Http as ResponseHttp;
use Magento\Framework\App\Request\Http as RequestHttp;
use Magento\Framework\Encryption\EncryptorInterface;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\Debug;
use Magento\Framework\Filesystem;
use Psr\Log\LoggerInterface;
use Magento\Framework\Exception\SessionException;
use Magento\Framework\Exception\State\InitException;

/**
 * Handler of HTTP web application exception
 */
class ExceptionHandler implements ExceptionHandlerInterface
{
    /**
     * @var Filesystem
     */
    private $filesystem;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @var EncryptorInterface
     */
    private $encryptor;

    /**
     * @param EncryptorInterface $encryptor
     * @param Filesystem $filesystem
     * @param LoggerInterface $logger
     */
    public function __construct(
        EncryptorInterface $encryptor,
        Filesystem $filesystem,
        LoggerInterface $logger
    ) {
        $this->encryptor = $encryptor;
        $this->filesystem = $filesystem;
        $this->logger = $logger;
    }

    /**
     * Handles exception of HTTP web application
     *
     * @param Bootstrap $bootstrap
     * @param \Exception $exception
     * @param ResponseHttp $response
     * @param RequestHttp $request
     * @return bool
     */
    public function handle(
        Bootstrap $bootstrap,
        \Exception $exception,
        ResponseHttp $response,
        RequestHttp $request
    ): bool {
        $result = $this->handleDeveloperMode($bootstrap, $exception, $response)
            || $this->handleBootstrapErrors($bootstrap, $exception, $response)
            || $this->handleSessionException($exception, $response, $request)
            || $this->handleInitException($exception)
            || $this->handleGenericReport($bootstrap, $exception);
        return $result;
    }

    /**
     * Error handler for developer mode
     *
     * @param Bootstrap $bootstrap
     * @param \Exception $exception
     * @param ResponseHttp $response
     * @return bool
     */
    private function handleDeveloperMode(
        Bootstrap $bootstrap,
        \Exception $exception,
        ResponseHttp $response
    ): bool {
        if ($bootstrap->isDeveloperMode()) {
            if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
                try {
                    $this->redirectToSetup($bootstrap, $exception, $response);
                    return true;
                } catch (\Exception $e) {
                    $exception = $e;
                }
            }
            $response->clearHeader('Location');
            $response->setHttpResponseCode(500);
            $response->setHeader('Content-Type', 'text/plain');
            $response->setBody($this->buildContentFromException($exception));
            $response->sendResponse();
            return true;
        }
        return false;
    }

    /**
     * Build content based on an exception
     *
     * @param \Exception $exception
     * @return string
     */
    private function buildContentFromException(\Exception $exception): string
    {
        /** @var \Exception[] $exceptions */
        $exceptions = [];

        do {
            $exceptions[] = $exception;
        } while ($exception = $exception->getPrevious());

        $buffer = sprintf("%d exception(s):\n", count($exceptions));

        foreach ($exceptions as $index => $exception) {
            $buffer .= sprintf(
                "Exception #%d (%s): %s\n",
                $index,
                get_class($exception),
                $exception->getMessage()
            );
        }

        foreach ($exceptions as $index => $exception) {
            $buffer .= sprintf(
                "\nException #%d (%s): %s\n%s\n",
                $index,
                get_class($exception),
                $exception->getMessage(),
                Debug::trace(
                    $exception->getTrace(),
                    true,
                    true,
                    (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
                )
            );
        }

        return $buffer;
    }

    /**
     * Handler for bootstrap errors
     *
     * @param Bootstrap $bootstrap
     * @param \Exception $exception
     * @param ResponseHttp $response
     * @return bool
     */
    private function handleBootstrapErrors(
        Bootstrap $bootstrap,
        \Exception &$exception,
        ResponseHttp $response
    ): bool {
        $bootstrapCode = $bootstrap->getErrorCode();
        if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
            // phpcs:ignore Magento2.Security.IncludeFile
            require $this->filesystem
                ->getDirectoryRead(DirectoryList::PUB)
                ->getAbsolutePath('errors/503.php');
            return true;
        }
        if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
            try {
                $this->redirectToSetup($bootstrap, $exception, $response);
                return true;
            } catch (\Exception $e) {
                $exception = $e;
            }
        }
        return false;
    }

    /**
     * Handler for session errors
     *
     * @param \Exception $exception
     * @param ResponseHttp $response
     * @param RequestHttp $request
     * @return bool
     */
    private function handleSessionException(
        \Exception $exception,
        ResponseHttp $response,
        RequestHttp $request
    ): bool {
        if ($exception instanceof SessionException) {
            $response->setRedirect($request->getDistroBaseUrl());
            $response->sendHeaders();
            return true;
        }
        return false;
    }

    /**
     * Handler for application initialization errors
     *
     * @param \Exception $exception
     * @return bool
     */
    private function handleInitException(\Exception $exception): bool
    {
        if ($exception instanceof InitException) {
            $this->logger->critical($exception);
            // phpcs:ignore Magento2.Security.IncludeFile
            require $this->filesystem
                ->getDirectoryRead(DirectoryList::PUB)
                ->getAbsolutePath('errors/404.php');
            return true;
        }
        return false;
    }

    /**
     * Handle for any other errors
     *
     * @param Bootstrap $bootstrap
     * @param \Exception $exception
     * @return bool
     */
    private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception): bool
    {
        $reportData = [
            $exception->getMessage(),
            Debug::trace(
                $exception->getTrace(),
                true,
                false,
                (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
            )
        ];
        $params = $bootstrap->getParams();
        if (isset($params['REQUEST_URI'])) {
            $reportData['url'] = $params['REQUEST_URI'];
        }
        if (isset($params['SCRIPT_NAME'])) {
            $reportData['script_name'] = $params['SCRIPT_NAME'];
        }
        $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData));
        $this->logger->critical($exception, ['report_id' => $reportData['report_id']]);
        // phpcs:ignore Magento2.Security.IncludeFile
        require $this->filesystem
            ->getDirectoryRead(DirectoryList::PUB)
            ->getAbsolutePath('errors/report.php');
        return true;
    }

    /**
     * If not installed, try to redirect to installation wizard
     *
     * @param Bootstrap $bootstrap
     * @param \Exception $exception
     * @param ResponseHttp $response
     * @return void
     * @throws \Exception
     */
    private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response)
    {
        $setupInfo = new SetupInfo($bootstrap->getParams());
        $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
        if ($setupInfo->isAvailable()) {
            $response->setRedirect($setupInfo->getUrl());
            $response->sendHeaders();
        } else {
            $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
                . "because the Magento setup directory cannot be accessed. \n"
                . 'You can install Magento using either the command line or you must restore access '
                . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
            // phpcs:ignore Magento2.Exceptions.DirectThrow
            throw new \Exception($newMessage, 0, $exception);
        }
    }
}
