<?php
namespace App\Services;

use App\Core\Config;
use RuntimeException;

/**
 * Validates and stores an uploaded file (FR-SUP-007, FR-SUP-040). Files are
 * stored outside the web root (BRD 7.3 data rule) — the ticket only ever
 * holds a reference to a generated path.
 */
class AttachmentService
{
    public static function store(array $file, int $ticketId): array
    {
        if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
            throw new RuntimeException('Upload failed.');
        }

        $maxSize = (int)Config::get('uploads.max_size_bytes', 10 * 1024 * 1024);
        if ($file['size'] > $maxSize) {
            throw new RuntimeException('File exceeds the maximum allowed size.');
        }

        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        $allowed = Config::get('uploads.allowed_extensions', []);
        if (!in_array($ext, $allowed, true)) {
            throw new RuntimeException("File type .$ext is not permitted.");
        }

        $baseDir = rtrim(Config::get('uploads.path'), '/');
        $ticketDir = $baseDir . '/' . $ticketId;
        if (!is_dir($ticketDir) && !mkdir($ticketDir, 0750, true) && !is_dir($ticketDir)) {
            throw new RuntimeException('Could not create upload directory.');
        }

        $storedName = bin2hex(random_bytes(16)) . '.' . $ext;
        $destination = $ticketDir . '/' . $storedName;

        if (is_uploaded_file($file['tmp_name'])) {
            if (!move_uploaded_file($file['tmp_name'], $destination)) {
                throw new RuntimeException('Could not save the uploaded file.');
            }
        } else {
            // Allows programmatic attachment (e.g. from the email-ingest cron job).
            if (!copy($file['tmp_name'], $destination)) {
                throw new RuntimeException('Could not save the file.');
            }
        }

        $scan = self::scanForMalware($destination);
        if ($scan === 'infected') {
            unlink($destination);
            throw new RuntimeException('This file was rejected by the malware scanner.');
        }

        return [
            'original_name' => $file['name'],
            'stored_path' => $ticketId . '/' . $storedName,
            'size_bytes' => $file['size'],
            // Detected server-side from the file's actual bytes, not the client-supplied
            // Content-Type — a browser's declared type is untrusted input and would
            // otherwise flow straight into the Content-Type header on download.
            'mime_type' => mime_content_type($destination) ?: ($file['type'] ?: null),
        ];
    }

    public static function absolutePath(string $storedPath): string
    {
        return rtrim(Config::get('uploads.path'), '/') . '/' . $storedPath;
    }

    /** Streams an attachment row to the browser as a forced download. Shared by the
     *  agent and portal controllers so the security-relevant headers live in one place. */
    public static function streamDownload(array $attachment): void
    {
        $path = self::absolutePath($attachment['stored_path']);
        if (!is_file($path)) {
            http_response_code(404);
            exit;
        }

        // Strip quotes/control characters so a crafted original filename can't break out
        // of the quoted Content-Disposition value or inject stray header-line characters.
        $safeName = preg_replace('/[\x00-\x1F"\\\\]/', '_', basename($attachment['original_name']));

        header('Content-Type: ' . ($attachment['mime_type'] ?: 'application/octet-stream'));
        header('X-Content-Type-Options: nosniff');
        header('Content-Disposition: attachment; filename="' . $safeName . '"');
        header('Content-Length: ' . filesize($path));
        readfile($path);
        exit;
    }

    /**
     * Best-effort malware scan (NFR-12). Uses the `clamscan` CLI when it's
     * installed (cPanel's ClamAV plugin puts it on PATH); silently skipped
     * otherwise rather than blocking uploads on hosts without it configured.
     * Returns 'clean', 'infected', or 'unavailable'.
     */
    private static function scanForMalware(string $path): string
    {
        static $clamAvailable = null;
        if ($clamAvailable === null) {
            $clamAvailable = function_exists('exec') && trim((string)@shell_exec('command -v clamscan')) !== '';
        }
        if (!$clamAvailable) {
            return 'unavailable';
        }

        exec('clamscan --no-summary ' . escapeshellarg($path), $output, $exitCode);
        // clamscan exit codes: 0 = clean, 1 = virus found, 2+ = scan error (treated as unavailable).
        if ($exitCode === 1) return 'infected';
        return $exitCode === 0 ? 'clean' : 'unavailable';
    }
}
