<?php
/**
 * Cache module — Environment detection
 *
 * Inspects the runtime environment (PHP, server, capabilities) to inform other
 * components about what they can or cannot safely do.
 *
 * @package BlaminhorEssentials
 */

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

/**
 * Detects server type and PHP capabilities.
 */
class Blaminhor_Essentials_Cache_Environment {

	/**
	 * Detected server software.
	 *
	 * @var string One of 'apache', 'nginx', 'litespeed', 'iis', 'unknown'.
	 */
	private $server_software;

	/**
	 * Cached capabilities flags.
	 *
	 * @var array<string, bool>|null
	 */
	private $capabilities = null;

	public function __construct() {
		$this->server_software = $this->detect_server_software();
	}

	/**
	 * @return string Server software identifier.
	 */
	public function get_server_software() {
		return $this->server_software;
	}

	/**
	 * Whether the current server uses .htaccess (Apache / LiteSpeed).
	 *
	 * @return bool
	 */
	public function supports_htaccess() {
		return in_array( $this->server_software, array( 'apache', 'litespeed' ), true );
	}

	/**
	 * Get all detected capabilities at once. Cached after first call.
	 *
	 * @return array<string, bool>
	 */
	public function get_capabilities() {
		if ( null !== $this->capabilities ) {
			return $this->capabilities;
		}

		$wp_config_path = $this->get_wp_config_path();

		$this->capabilities = array(
			'openssl'           => function_exists( 'openssl_encrypt' ),
			'gzencode'          => function_exists( 'gzencode' ),
			'mbstring'          => function_exists( 'mb_strlen' ),
			'dom_document'      => class_exists( 'DOMDocument' ),
			'curl'              => function_exists( 'curl_init' ),
			'htaccess'          => $this->supports_htaccess(),
			'wp_content_writable' => is_writable( WP_CONTENT_DIR ),
			'wp_config_writable' => null !== $wp_config_path && is_writable( $wp_config_path ),
			'wp_config_found'   => null !== $wp_config_path,
			'multisite'         => is_multisite(),
		);

		return $this->capabilities;
	}

	/**
	 * @param string $cap Capability key.
	 * @return bool
	 */
	public function has( $cap ) {
		$caps = $this->get_capabilities();
		return isset( $caps[ $cap ] ) ? (bool) $caps[ $cap ] : false;
	}

	/**
	 * Locate wp-config.php. WordPress places it either in ABSPATH or one
	 * directory above (single-user shared hosting).
	 *
	 * @return string|null Absolute path or null if not found.
	 */
	public function get_wp_config_path() {
		$candidate_a = ABSPATH . 'wp-config.php';
		if ( file_exists( $candidate_a ) ) {
			return $candidate_a;
		}
		$candidate_b = dirname( ABSPATH ) . '/wp-config.php';
		if ( file_exists( $candidate_b ) && ! file_exists( dirname( ABSPATH ) . '/wp-settings.php' ) ) {
			return $candidate_b;
		}
		return null;
	}

	/**
	 * Read wp-config.php contents. Returns null if unreadable.
	 *
	 * @return string|null
	 */
	public function read_wp_config() {
		$path = $this->get_wp_config_path();
		if ( null === $path || ! is_readable( $path ) ) {
			return null;
		}
		$contents = file_get_contents( $path );
		return false === $contents ? null : $contents;
	}

	/**
	 * Replace wp-config.php contents atomically. Returns true on success.
	 *
	 * We deliberately do NOT bail on `! is_writable( $path )`: shared hosts
	 * (Infomaniak, OVH, etc.) frequently ship wp-config.php in 0644 with the
	 * PHP user as owner, where `is_writable()` reads false negative but the
	 * actual write+rename succeeds. WP Rocket / WP Super Cache use the same
	 * tente-et-vois strategy. Permissions on the *parent directory* matter
	 * more than on the file itself for atomic rename.
	 *
	 * @param string $contents New contents.
	 * @return bool
	 */
	public function write_wp_config( $contents ) {
		$path = $this->get_wp_config_path();
		if ( null === $path ) {
			return false;
		}

		// Atomic write via temp + rename to avoid partial writes if the host
		// kills the process mid-write (would brick the site).
		$tmp = $path . '.be-tmp-' . wp_generate_password( 6, false, false );
		if ( false === @file_put_contents( $tmp, $contents, LOCK_EX ) ) {
			return false;
		}

		// rename() over an existing file requires write permission on the
		// PARENT DIRECTORY, not on the target file. So this works even when
		// wp-config.php is 0644 owned by us in a 0755 dir owned by us.
		if ( ! @rename( $tmp, $path ) ) {
			// Fallback: if rename failed (some FS / hosts disallow rename
			// over an existing file), try a chmod-write-restore sequence
			// directly on $path.
			@unlink( $tmp );
			$old_perms = @fileperms( $path ) & 0777;
			if ( $old_perms && @chmod( $path, 0664 ) ) {
				$ok = (bool) @file_put_contents( $path, $contents, LOCK_EX );
				@chmod( $path, $old_perms );
				if ( ! $ok ) {
					return false;
				}
			} else {
				return false;
			}
		}

		// Make sure the file is readable (some umasks produce 0600 on tmp).
		$target_perms = defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644;
		@chmod( $path, $target_perms );

		// Invalidate OPcache so the new constant is seen immediately.
		if ( function_exists( 'opcache_invalidate' ) ) {
			@opcache_invalidate( $path, true );
		}
		return true;
	}

	/**
	 * Detect the underlying server.
	 *
	 * @return string
	 */
	private function detect_server_software() {
		$sig = isset( $_SERVER['SERVER_SOFTWARE'] ) ? strtolower( sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) ) : '';
		if ( strpos( $sig, 'litespeed' ) !== false ) {
			return 'litespeed';
		}
		if ( strpos( $sig, 'apache' ) !== false ) {
			return 'apache';
		}
		if ( strpos( $sig, 'nginx' ) !== false ) {
			return 'nginx';
		}
		if ( strpos( $sig, 'iis' ) !== false || strpos( $sig, 'microsoft' ) !== false ) {
			return 'iis';
		}
		return 'unknown';
	}
}
