<?php

namespace Velis\Filesystem\Av\Concrete;

use Exception;
use Velis\Filesystem\Av\Contract\AvClientInterface;
use Velis\Filesystem\Av\Contract\ConnectionInterface;
use Velis\Filesystem\Av\Contract\FileHandlerInterface;
use Velis\Filesystem\Av\Exception\OneOrMoreFilesInfectedException;

/**
 * @author Szymon Janaczek <szymon.janaczek@velistech.com>
 * inspired by @url https://github.com/appwrite/php-clamav/tree/master
 */
class ClamAvClient implements AvClientInterface
{
    public function __construct(
        private readonly ConnectionInterface $connection,
        private readonly FileHandlerInterface $fileHandler,
        private readonly int $maxResponseSize = 20000,
    ) {
    }

    /**
     * Send a given command to ClamAV.
     */
    public function sendCommand(string $command): ?string
    {
        $response = '';

        $socket = $this->connection->getSocket();

        $socket->send($command, strlen($command), 0);
        $socket->recv($response, $this->maxResponseSize, 0);
        $socket->close();

        if (!is_string($response) || empty($response)) {
            return null;
        }

        return trim($response);
    }

    /**
     * Check if ClamAV is up and responsive.
     */
    public function ping(): bool
    {
        try {
            $response = $this->sendCommand('PING');
        } catch (Exception $e) {
            return false;
        }

        return $response === 'PONG';
    }

    /**
     * @inheritDoc
     * with archive support enabled (if not disabled in clamd.conf)
     */
    public function fileScanInStream(string $filePath): AvScanResult
    {
        ['handle' => $handle, 'fileSize' => $fileSize] = $this->fileHandler->processFile($filePath);

        $scannerHandler = $this->connection->getSocket()->socketExportStream();

        // Push to the ClamAV socket.
        // Disabled PHP-CS rule because we have checked this in the processFile method.
        // phpcs:disable PHPCS_SecurityAudit.BadFunctions.FilesystemFunctions.WarnFilesystem
        fwrite($scannerHandler, "zINSTREAM\0");
        fwrite($scannerHandler, pack("N", $fileSize));
        stream_copy_to_stream($handle, $scannerHandler);

        // Send a zero-length block to indicate that we're done sending file data.
        fwrite($scannerHandler, pack("N", 0));

        // Request a response from the service.
        $response = trim(fgets($scannerHandler));

        fclose($scannerHandler);
        // phpcs:enable

        $isInfected = $response !== 'stream: OK';

        return AvScanResult::fromRaw(isInfected: $isInfected);
    }
}
