<?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\CoreBundle\Comment;

use Contao\CoreBundle\Filesystem\VirtualFilesystemInterface;
use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\CoreBundle\Entity\ConversationItem;
use Ferienpass\CoreBundle\Entity\ConversationItemAttachment;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
 * Takes the attachment descriptors parsed off an inbound email and, for each,
 * downloads → security-checks → stores in the tenant-isolated S3
 * `inbound_attachments` filesystem → links a {@see ConversationItemAttachment}
 * to the reply. Anything that fails a step is dropped and counted, never stored.
 */
final readonly class InboundAttachmentIngest
{
    public function __construct(
        private BrevoAttachmentDownloader $downloader,
        private InboundAttachmentPolicy $policy,
        #[Autowire(service: 'contao.filesystem.virtual.inbound_attachments')]
        private VirtualFilesystemInterface $storage,
        private EntityManagerInterface $em,
        private LoggerInterface $logger,
    ) {
    }

    /**
     * @param array<int, array<string, mixed>> $attachments descriptors: {token, name, mime, size}
     *
     * @return array{stored: int, blocked: int}
     */
    public function ingest(ConversationItem $item, array $attachments): array
    {
        $stored = 0;
        $blocked = 0;

        foreach (\array_slice($attachments, 0, InboundAttachmentPolicy::MAX_COUNT) as $descriptor) {
            $token = (string) ($descriptor['token'] ?? '');
            $name = trim((string) ($descriptor['name'] ?? '')) ?: 'anhang';
            $declaredSize = (int) ($descriptor['size'] ?? 0);

            // Cheap rejections before spending a download on them.
            if ('' === $token || $declaredSize > InboundAttachmentPolicy::MAX_BYTES) {
                ++$blocked;
                continue;
            }

            $bytes = $this->downloader->download($token, InboundAttachmentPolicy::MAX_BYTES);
            if (null === $bytes) {
                ++$blocked;
                continue;
            }

            $decision = $this->policy->validate($name, $bytes);
            if (!($decision['ok'] ?? false)) {
                $this->logger->notice('Inbound attachment blocked.', ['name' => $name, 'reason' => $decision['reason'] ?? '']);
                ++$blocked;
                continue;
            }

            $path = $item->getUuid()->toBase58().'/'.bin2hex(random_bytes(8)).'.'.$decision['ext'];

            try {
                $this->storage->write($path, $bytes);
            } catch (\Throwable $e) {
                $this->logger->error('Storing inbound attachment failed: '.$e->getMessage());
                ++$blocked;
                continue;
            }

            $this->em->persist(new ConversationItemAttachment($item, $path, $decision['name'], $decision['mime'], $decision['size']));
            ++$stored;
        }

        return ['stored' => $stored, 'blocked' => $blocked];
    }
}
