<?php

namespace Licenzo\Services\Media;

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Streams files to the client with proper resource management.
 *
 * Delivery priority:
 * 1. X-Sendfile (Apache mod_xsendfile) — PHP process exits immediately
 * 2. Chunked fread() — 8 KB chunks, connection-abort-aware, no timeout risk
 */
class FileStreamer
{
    private const CHUNK_SIZE = 8192;

    /**
     * Stream a file to the client and terminate the request.
     *
     * @param string   $filePath Absolute path to the file on disk.
     * @param string   $fileName Name presented to the browser in Content-Disposition.
     * @param string   $mimeType MIME type for Content-Type header.
     * @param int|null $fileSize File size in bytes (null to omit Content-Length).
     *
     * @return never
     */
    public static function stream(string $filePath, string $fileName, string $mimeType, ?int $fileSize = null): void
    {
        if (session_status() === PHP_SESSION_ACTIVE) {
            session_write_close();
        }

        while (ob_get_level() > 0) {
            ob_end_clean();
        }

        if (static::tryServerDelivery($filePath, $fileName, $mimeType, $fileSize)) {
            exit;
        }

        static::chunkedStream($filePath, $fileName, $mimeType, $fileSize);
        exit;
    }

    /**
     * Attempt to offload file delivery to the web server.
     */
    private static function tryServerDelivery(string $filePath, string $fileName, string $mimeType, ?int $fileSize): bool
    {
        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_apache_get_modules
        if (function_exists('apache_get_modules') && in_array('mod_xsendfile', apache_get_modules(), true)) {
            static::sendHeaders($fileName, $mimeType, $fileSize);
            header('X-Sendfile: ' . $filePath);
            return true;
        }

        return false;
    }

    /**
     * Read and output the file in small chunks.
     *
     * Each 8 KB read completes in microseconds, so PHP's execution time limit
     * is never at risk. connection_aborted() checks between chunks free the
     * worker immediately when the client disconnects.
     */
    private static function chunkedStream(string $filePath, string $fileName, string $mimeType, ?int $fileSize): void
    {
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Direct binary streaming to authenticated client.
        $handle = fopen($filePath, 'rb');
        if (!$handle) {
            throw new \RuntimeException('Unable to open file for streaming.');
        }

        static::sendHeaders($fileName, $mimeType, $fileSize);
        ignore_user_abort(false);

        try {
            // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
            while (!feof($handle)) {
                if (connection_aborted()) {
                    break;
                }

                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
                $chunk = fread($handle, static::CHUNK_SIZE);
                if ($chunk === false) {
                    break;
                }

                // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Binary file stream; escaping would corrupt download data.
                echo $chunk;
                flush();
            }
        } finally {
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
            fclose($handle);
        }
    }

    private static function sendHeaders(string $fileName, string $mimeType, ?int $fileSize): void
    {
        nocache_headers();
        header('Content-Type: ' . $mimeType);
        header('Content-Disposition: attachment; filename="' . $fileName . '"');
        if ($fileSize !== null && $fileSize > 0) {
            header('Content-Length: ' . $fileSize);
        }
        header('X-Content-Type-Options: nosniff');
    }
}
