<?php

declare(strict_types=1);

namespace App\Shared\Infrastructure\Upload;

use App\Shared\Domain\Entity\UploadedDocument;
use App\Shared\Domain\Exception\FileTooLargeException;
use App\Shared\Domain\Exception\FileUploadException;
use App\Shared\Domain\Exception\UnsupportedMimeTypeException;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
 * Service generique de televersement de fichiers (infra Shared, ERP-154).
 *
 * Responsabilites :
 *  - Validation du type MIME SERVER-SIDE via `getMimeType()` (detection finfo
 *    sur le contenu reel) — JAMAIS `getClientMimeType()`, spoofable par le
 *    client (regle backend.md « Upload de fichiers »).
 *  - Whitelist MIME explicite (PDF + images courantes).
 *  - Bornage de la taille (MAX_SIZE_BYTES).
 *  - Calcul du checksum sha256 (controle d integrite) AVANT le deplacement.
 *  - Ecriture disque sous `var/uploads/{yyyy}/{mm}/` avec un nom genere
 *    aleatoirement (jamais le nom client, qui reste une simple metadonnee).
 *
 * Le service est volontairement decouple de HTTP au-dela du type UploadedFile :
 * il leve des exceptions metier (FileUploadException), traduites en 422 par le
 * UploadedDocumentProcessor.
 */
final class FileUploader
{
    /**
     * Types MIME autorises (detectes server-side) : PDF + images courantes.
     *
     * @var list<string>
     */
    public const ALLOWED_MIME_TYPES = [
        'application/pdf',
        'image/jpeg',
        'image/png',
        'image/webp',
        'image/gif',
    ];

    /**
     * Taille maximale autorisee : 10 Mo.
     */
    public const MAX_SIZE_BYTES = 10 * 1024 * 1024;

    public function __construct(
        // Racine de stockage des fichiers televerses (hors web root, sous var/).
        #[Autowire('%kernel.project_dir%/var/uploads')]
        private readonly string $uploadBaseDir,
        private readonly ClockInterface $clock,
    ) {}

    /**
     * Valide, calcule l empreinte, deplace le fichier sur disque et retourne
     * un UploadedDocument NON persiste (le caller le persiste).
     *
     * @throws UnsupportedMimeTypeException si le MIME server-side est hors whitelist
     * @throws FileTooLargeException        si le fichier depasse MAX_SIZE_BYTES
     */
    public function upload(UploadedFile $file): UploadedDocument
    {
        // Detection MIME server-side (finfo sur le contenu) — jamais le MIME
        // declare par le client.
        $mimeType = $file->getMimeType() ?? 'application/octet-stream';
        if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true)) {
            throw new UnsupportedMimeTypeException($mimeType, self::ALLOWED_MIME_TYPES);
        }

        // getSize() peut renvoyer false si le fichier est illisible.
        $size = $file->getSize();
        if (false === $size || $size > self::MAX_SIZE_BYTES) {
            throw new FileTooLargeException(false === $size ? 0 : $size, self::MAX_SIZE_BYTES);
        }

        // Checksum AVANT le move : le chemin du fichier change apres deplacement.
        // hash_file renvoie false si le fichier temporaire est illisible (I/O) :
        // on echoue proprement plutot que de propager un TypeError opaque au
        // constructeur (parametre $checksum type string).
        $checksum = hash_file('sha256', $file->getPathname());
        if (false === $checksum) {
            throw new FileUploadException('Impossible de lire le fichier televerse pour en calculer l\'empreinte.');
        }

        $now         = $this->clock->now();
        $relativeDir = $now->format('Y').'/'.$now->format('m');
        $targetDir   = $this->uploadBaseDir.'/'.$relativeDir;

        // Nom de stockage genere aleatoirement : evite les collisions et toute
        // injection via le nom client. Extension deduite du MIME.
        $extension  = $file->guessExtension() ?: 'bin';
        $storedName = bin2hex(random_bytes(16)).'.'.$extension;

        // Le nom d origine est conserve uniquement comme metadonnee d affichage,
        // borne a la longueur de colonne (255).
        $originalFilename = mb_substr($file->getClientOriginalName(), 0, 255);

        $file->move($targetDir, $storedName);

        return new UploadedDocument(
            originalFilename: $originalFilename,
            storedPath: $relativeDir.'/'.$storedName,
            mimeType: $mimeType,
            sizeBytes: $size,
            checksum: $checksum,
            createdAt: $now,
        );
    }

    /**
     * Supprime le fichier physique d'un document (compensation lorsque la
     * persistance echoue APRES l'ecriture disque). Best-effort : silencieux si
     * le fichier a deja disparu. Evite d'accumuler des binaires orphelins non
     * references en base.
     */
    public function remove(UploadedDocument $document): void
    {
        $path = $this->uploadBaseDir.'/'.$document->getStoredPath();
        if (is_file($path)) {
            @unlink($path);
        }
    }
}
