<?php

declare(strict_types=1);

namespace SecurWP\Includes;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Helpers for storing plugin-owned runtime artifacts in PHP-guarded files.
 *
 * The payload always lives after a `__halt_compiler()` header so direct web
 * requests execute the guard and stop before any bytes are exposed.
 */
class GuardedDataFile {

	private const HEADER = "<?php exit('Access denied'); __halt_compiler(); ?>\n";

	public static function header(): string {
		return self::HEADER;
	}

	public static function header_length(): int {
		return strlen( self::HEADER );
	}

	public static function has_guard_prefix( string $contents ): bool {
		return strncmp( $contents, self::HEADER, self::header_length() ) === 0;
	}

	public static function strip_guard( string $contents ): string {
		if ( self::has_guard_prefix( $contents ) ) {
			return substr( $contents, self::header_length() );
		}

		return $contents;
	}

	public static function payload_offset_for_path( string $path ): int {
		$prefix = @file_get_contents( $path, false, null, 0, self::header_length() );
		if ( ! is_string( $prefix ) ) {
			return 0;
		}

		return self::has_guard_prefix( $prefix ) ? self::header_length() : 0;
	}

	/**
	 * @return string|false
	 */
	public static function read_payload( string $path ) {
		$contents = @file_get_contents( $path );
		if ( ! is_string( $contents ) ) {
			return false;
		}

		return self::strip_guard( $contents );
	}

	public static function write_payload_atomic( string $path, string $payload, ?int $mode = null ): bool {
		if ( ! self::ensure_parent_directory( $path ) ) {
			return false;
		}

		$temp_path = $path . '.tmp.' . bin2hex( random_bytes( 4 ) );
		$bytes     = @file_put_contents( $temp_path, self::HEADER . $payload, LOCK_EX );
		if ( false === $bytes ) {
			wp_delete_file( $temp_path );
			return false;
		}

		if ( is_int( $mode ) && $mode > 0 ) {
			@chmod( $temp_path, $mode ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod
		}

		if ( ! @rename( $temp_path, $path ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename
			wp_delete_file( $temp_path );
			return false;
		}

		return true;
	}

	public static function clear_payload( string $path, ?int $mode = null ): bool {
		return self::write_payload_atomic( $path, '', $mode );
	}

	public static function append_payload( string $path, string $payload, ?int $mode = null ): bool {
		if ( ! self::ensure_parent_directory( $path ) ) {
			return false;
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Guarded append requires a lockable handle.
		$handle = @fopen( $path, 'c+' );
		if ( false === $handle ) {
			return false;
		}

		$ok = false;

		try {
			if ( ! flock( $handle, LOCK_EX ) ) {
				return false;
			}

			if ( ! self::ensure_guarded_stream( $handle ) ) {
				return false;
			}

			if ( 0 !== fseek( $handle, 0, SEEK_END ) ) {
				return false;
			}

			$ok = self::write_all( $handle, $payload );
			fflush( $handle );
		} finally {
			flock( $handle, LOCK_UN );
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closes the guarded append handle after the locked write.
			fclose( $handle );
		}

		if ( $ok && is_int( $mode ) && $mode > 0 ) {
			@chmod( $path, $mode ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod
		}

		return $ok;
	}

	/**
	 * @return string|false
	 */
	public static function drain_payload( string $path ) {
		if ( ! file_exists( $path ) ) {
			return '';
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Guarded drain requires a lockable handle.
		$handle = @fopen( $path, 'c+' );
		if ( false === $handle ) {
			return false;
		}

		$payload = false;

		try {
			if ( ! flock( $handle, LOCK_EX ) ) {
				return false;
			}

			if ( ! self::ensure_guarded_stream( $handle ) ) {
				return false;
			}

			if ( 0 !== fseek( $handle, self::header_length() ) ) {
				return false;
			}

			$payload = stream_get_contents( $handle );
			if ( false === $payload ) {
				return false;
			}

			if ( ! ftruncate( $handle, 0 ) || 0 !== fseek( $handle, 0 ) || ! self::write_all( $handle, self::HEADER ) ) {
				return false;
			}

			fflush( $handle );
		} finally {
			flock( $handle, LOCK_UN );
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closes the guarded drain handle after truncation.
			fclose( $handle );
		}

		return $payload;
	}

	public static function rotated_path( string $path, string $suffix ): string {
		if ( str_ends_with( $path, '.php' ) ) {
			return substr( $path, 0, -4 ) . $suffix . '.php';
		}

		return $path . $suffix;
	}

	public static function migrate_legacy_plaintext( string $legacy_path, string $guarded_path, ?int $mode = null ): bool {
		if ( ! file_exists( $legacy_path ) || file_exists( $guarded_path ) ) {
			return false;
		}

		$contents = @file_get_contents( $legacy_path );
		if ( ! is_string( $contents ) ) {
			return false;
		}

		if ( ! self::write_payload_atomic( $guarded_path, $contents, $mode ) ) {
			return false;
		}

		wp_delete_file( $legacy_path );
		return true;
	}

	private static function ensure_parent_directory( string $path ): bool {
		$dir = dirname( $path );
		if ( is_dir( $dir ) ) {
			return true;
		}

		return wp_mkdir_p( $dir );
	}

	/**
	 * @param resource $handle
	 */
	private static function ensure_guarded_stream( $handle ): bool {
		if ( 0 !== fseek( $handle, 0 ) ) {
			return false;
		}

		$prefix = fread( $handle, self::header_length() ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
		if ( is_string( $prefix ) && self::has_guard_prefix( $prefix ) ) {
			return true;
		}

		if ( 0 !== fseek( $handle, 0 ) ) {
			return false;
		}

		$existing = stream_get_contents( $handle );
		if ( false === $existing ) {
			$existing = '';
		}

		if ( ! ftruncate( $handle, 0 ) || 0 !== fseek( $handle, 0 ) ) {
			return false;
		}

		return self::write_all( $handle, self::HEADER . $existing );
	}

	/**
	 * @param resource $handle
	 */
	private static function write_all( $handle, string $data ): bool {
		$written = 0;
		$length  = strlen( $data );

		while ( $written < $length ) {
			$result = fwrite( $handle, substr( $data, $written ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
			if ( false === $result || 0 === $result ) {
				return false;
			}

			$written += $result;
		}

		return true;
	}
}
