<?php
/**
 * Bounded file content reader.
 *
 * @package LocalGuardScanner
 */

namespace LocalGuardScanner\Detection\Filesystem;

use RuntimeException;

/**
 * Reads text candidates up to a strict size ceiling.
 */
final class FileContentReader {
	const DEFAULT_MAX_BYTES = 5242880;

	/**
	 * @param string              $path      File path.
	 * @param int                 $max_bytes Maximum bytes.
	 * @param array<string,mixed> $expected  Enumerated file identity.
	 * @return string
	 */
	public function read( $path, $max_bytes = self::DEFAULT_MAX_BYTES, array $expected = array() ) {
		$size = isset( $expected['size'] ) ? (int) $expected['size'] : @filesize( $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
		if ( false === $size || $size < 0 || $size > $max_bytes ) {
			throw new RuntimeException( 'File exceeds the deep-scan size limit or cannot be measured.' );
		}
		$guard          = new FileIdentityGuard();
		$handle         = $guard->open( $path, $expected );
		$content        = '';
		$content_length = 0;
		try {
			while ( ! feof( $handle ) && $content_length <= $max_bytes ) {
				$read_length = max( 1, min( 65536, $max_bytes + 1 - $content_length ) );
				$chunk       = fread( $handle, $read_length ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
				if ( false === $chunk ) {
					throw new RuntimeException( 'File cannot be read within the deep-scan limit.' );
				}
				if ( '' === $chunk && ! feof( $handle ) ) {
					throw new RuntimeException( 'File reading stopped before reaching a safe end.' );
				}
				$content        .= $chunk;
				$content_length += strlen( $chunk );
			}
			if ( $content_length > $max_bytes ) {
				throw new RuntimeException( 'File exceeds the deep-scan size limit.' );
			}
			$guard->assert_stable( $handle, $expected );
		} finally {
			fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
		}
		return $content;
	}
}
