<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\AdminBundle\Controller;

use Contao\CoreBundle\Filesystem\VirtualFilesystemException;
use Contao\CoreBundle\Filesystem\VirtualFilesystemInterface;
use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\CoreBundle\Entity\ConversationItemAttachment;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Uid\Uuid;

/**
 * Hands an inbound email attachment to an admin. The bytes come from an
 * untrusted sender, so — no matter what the stored type is — the response is
 * always a forced download served as `application/octet-stream` with
 * `nosniff`/`DENY`, never rendered inline. Access is limited to ROLE_ADMIN
 * (host-only users, who never see the conversation, are excluded); the file id
 * is an unguessable v7 uuid.
 */
#[IsGranted('ROLE_ADMIN')]
final class ConversationAttachmentController extends AbstractController
{
    public function __construct(
        private readonly EntityManagerInterface $em,
        #[Autowire(service: 'contao.filesystem.virtual.inbound_attachments')]
        private readonly VirtualFilesystemInterface $storage,
    ) {
    }

    #[Route('/conversation-attachment/{uuid}', name: 'admin_conversation_attachment', methods: ['GET'])]
    public function __invoke(string $uuid): Response
    {
        try {
            $id = Uuid::fromBase58($uuid);
        } catch (\InvalidArgumentException) {
            throw $this->createNotFoundException();
        }

        $attachment = $this->em->find(ConversationItemAttachment::class, $id);
        if (!$attachment instanceof ConversationItemAttachment) {
            throw $this->createNotFoundException();
        }

        try {
            $stream = $this->storage->readStream($attachment->getPath());
        } catch (VirtualFilesystemException) {
            throw $this->createNotFoundException();
        }

        $response = new StreamedResponse(static function () use ($stream): void {
            stream_copy_to_stream($stream, fopen('php://output', 'w'));
        });

        $filename = $attachment->getName();
        $response->headers->set('Content-Type', 'application/octet-stream');
        $response->headers->set('X-Content-Type-Options', 'nosniff');
        $response->headers->set('X-Frame-Options', 'DENY');
        $response->headers->set('Content-Length', (string) $attachment->getSize());
        $response->headers->set('Content-Disposition', HeaderUtils::makeDisposition(
            HeaderUtils::DISPOSITION_ATTACHMENT,
            $filename,
            preg_replace('/[^\x20-\x7e]|[%\/\\\\]/', '_', $filename) ?: 'download',
        ));

        return $response;
    }
}
