<?php

namespace App\Services;

/**
 * PRC-360 — ClamAV scanner over the clamd INSTREAM TCP protocol.
 *
 * Talks to the `clamav` docker service (sail network, TCP 3310). The
 * antivirus_enabled setting is a graceful-degrade switch: when off, files
 * pass through as `clean` (feature disabled, no scanning). If clamd is
 * unreachable the result is `scan_failed` (admin can retry) — files are
 * NOT auto-cleared on a scanner outage.
 */
class AntivirusService
{
    public const CLEAN = 'clean';
    public const INFECTED = 'infected';
    public const FAILED = 'scan_failed';

    /**
     * @return array{status:string, message:?string}
     */
    public function scan(string $absolutePath): array
    {
        if (! setting('antivirus_enabled', false)) {
            return ['status' => self::CLEAN, 'message' => 'antivirus disabled'];
        }

        if (! is_readable($absolutePath)) {
            return ['status' => self::FAILED, 'message' => 'file not readable'];
        }

        $host = env('CLAMAV_HOST', 'clamav');
        $port = (int) env('CLAMAV_PORT', 3310);

        $socket = @fsockopen($host, $port, $errno, $errstr, 5);
        if (! $socket) {
            return ['status' => self::FAILED, 'message' => "clamd unreachable: {$errstr}"];
        }

        try {
            stream_set_timeout($socket, 30);
            fwrite($socket, "zINSTREAM\0");

            $fh = fopen($absolutePath, 'rb');
            if (! $fh) {
                return ['status' => self::FAILED, 'message' => 'cannot open file'];
            }
            while (! feof($fh)) {
                $chunk = fread($fh, 8192);
                if ($chunk === '' || $chunk === false) {
                    break;
                }
                // 4-byte network-order length prefix + chunk.
                fwrite($socket, pack('N', strlen($chunk)).$chunk);
            }
            fclose($fh);
            // Zero-length chunk terminates the stream.
            fwrite($socket, pack('N', 0));

            $response = trim((string) fgets($socket));
        } finally {
            fclose($socket);
        }

        if ($response === '' || str_contains($response, 'ERROR')) {
            return ['status' => self::FAILED, 'message' => $response ?: 'empty clamd response'];
        }
        if (str_contains($response, 'FOUND')) {
            // e.g. "stream: Win.Test.EICAR_HDB-1 FOUND"
            return ['status' => self::INFECTED, 'message' => $response];
        }
        if (str_contains($response, 'OK')) {
            return ['status' => self::CLEAN, 'message' => null];
        }

        return ['status' => self::FAILED, 'message' => $response];
    }
}
