<?php

declare(strict_types=1);

namespace App\Module\ProjectManagement\Infrastructure\Controller;

use App\Module\Integration\Domain\Exception\ShareConnectionException;
use App\Module\Integration\Domain\Exception\ShareNotConfiguredException;
use App\Module\Integration\Domain\Service\FileSource;
use App\Module\ProjectManagement\Domain\Entity\TaskDocument;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

use function is_resource;

class TaskDocumentDownloadController extends AbstractController
{
    public function __construct(
        private readonly EntityManagerInterface $entityManager,
        private readonly FileSource $fileSource,
        private readonly string $uploadDir,
    ) {}

    #[Route('/api/task_documents/{id}/download', name: 'task_document_download', methods: ['GET'], priority: 1)]
    #[IsGranted('IS_AUTHENTICATED_FULLY')]
    public function __invoke(int $id): Response
    {
        $document = $this->entityManager->getRepository(TaskDocument::class)->find($id);

        if (null === $document) {
            throw new NotFoundHttpException('Document not found.');
        }

        $mimeType = $document->getMimeType() ?? 'application/octet-stream';

        // Inline for images (except SVG) and PDFs, attachment for everything else.
        // SVG is always attachment to prevent XSS via embedded JavaScript.
        $inline      = 'image/svg+xml' !== $mimeType && (str_starts_with($mimeType, 'image/') || 'application/pdf' === $mimeType);
        $disposition = $inline ? ResponseHeaderBag::DISPOSITION_INLINE : ResponseHeaderBag::DISPOSITION_ATTACHMENT;

        return $document->isShareLink()
            ? $this->streamFromShare($document, $mimeType, $disposition)
            : $this->streamFromDisk($document, $mimeType, $disposition);
    }

    private function streamFromDisk(TaskDocument $document, string $mimeType, string $disposition): BinaryFileResponse
    {
        $filePath = $this->uploadDir.'/'.$document->getFileName();

        if (!file_exists($filePath)) {
            throw new NotFoundHttpException('File not found on disk.');
        }

        $response = new BinaryFileResponse($filePath);
        $response->setContentDisposition($disposition, (string) $document->getOriginalName());
        $response->headers->set('Content-Type', $mimeType);
        $response->headers->set('X-Content-Type-Options', 'nosniff');

        return $response;
    }

    private function streamFromShare(TaskDocument $document, string $mimeType, string $disposition): StreamedResponse
    {
        try {
            $stream = $this->fileSource->read((string) $document->getSharePath());
        } catch (ShareNotConfiguredException) {
            throw new NotFoundHttpException('Share not configured.');
        } catch (ShareConnectionException) {
            throw new NotFoundHttpException('File not found on the share.');
        }

        $response = new StreamedResponse(function () use ($stream): void {
            if (is_resource($stream)) {
                fpassthru($stream);
                fclose($stream);
            }
        });
        $response->headers->set('Content-Type', $mimeType);
        $response->headers->set('Content-Disposition', HeaderUtils::makeDisposition($disposition, (string) $document->getOriginalName()));
        $response->headers->set('X-Content-Type-Options', 'nosniff');

        return $response;
    }
}
