<?php
/**
 * /backreload/v1/system — environment / pre-flight check.
 *
 * Returns a list of named checks the UI renders as ✅/⚠️/❌. Used so users
 * (and we) can tell at a glance if a host is missing a required extension,
 * is too tight on memory, or is missing an optional SDK for a destination.
 *
 * @package Backreload\Api
 */

namespace Backreload\Api;

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

class SystemController extends BaseController {

	public function register( string $namespace ): void {
		register_rest_route( $namespace, '/system', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'show' ],
			'permission_callback' => [ $this, 'check_perm' ],
		] );

		// Self-service opcache reset. Shared-hosting users can't restart
		// PHP-FPM, so when a plugin update changes a file there's no
		// reliable way for them to force the new bytecode to load — they
		// have to wait for opcache.revalidate_freq to elapse (60s default
		// but sometimes much longer). This endpoint exposes opcache_reset()
		// behind manage_options so the user can click "Refresh PHP cache"
		// in the System page and immediately pick up new code.
		register_rest_route( $namespace, '/system/refresh-cache', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'refresh_cache' ],
			'permission_callback' => [ $this, 'check_perm' ],
		] );
	}

	public function refresh_cache() {
		$results = [];
		if ( function_exists( 'opcache_reset' ) ) {
			$ok = @opcache_reset();
			$results['opcache'] = $ok ? 'reset' : 'reset failed (may be disabled or already cleared)';
		} else {
			$results['opcache'] = 'not available on this PHP build';
		}
		// Bonus: also clear WP's object cache (some hosts cache options).
		if ( function_exists( 'wp_cache_flush' ) ) {
			wp_cache_flush();
			$results['wp_cache'] = 'flushed';
		}
		$this->plugin->get( 'logger' )->info( 'PHP cache refreshed by user', $results );
		return $this->ok( $results );
	}

	public function show() {
		$checks  = $this->run_checks();
		$summary = [
			'pass'  => 0,
			'warn'  => 0,
			'fail'  => 0,
			'total' => count( $checks ),
		];
		foreach ( $checks as $c ) {
			$summary[ $c['status'] ] = ( $summary[ $c['status'] ] ?? 0 ) + 1;
		}
		$summary['health_score'] = $summary['total']
			? (int) round( ( $summary['pass'] + ( $summary['warn'] * 0.5 ) ) / $summary['total'] * 100 )
			: 100;

		return $this->ok( [
			'checks'  => $checks,
			'summary' => $summary,
		] );
	}

	private function run_checks(): array {
		$checks = [];

		/* ─── Core (must work) ─────────────────────────────────────────── */

		$php = PHP_VERSION;
		$checks[] = [
			'id'          => 'php_version',
			'category'    => 'core',
			'label'       => 'PHP version',
			'value'       => $php,
			'requirement' => '7.4+ required, 8.1+ recommended',
			'status'      => version_compare( $php, '8.1', '>=' ) ? 'pass'
				: ( version_compare( $php, '7.4', '>=' ) ? 'warn' : 'fail' ),
		];

		global $wp_version;
		$checks[] = [
			'id'          => 'wp_version',
			'category'    => 'core',
			'label'       => 'WordPress version',
			'value'       => $wp_version,
			'requirement' => '6.0+',
			'status'      => version_compare( $wp_version, '6.0', '>=' ) ? 'pass' : 'fail',
		];

		$has_zip = class_exists( '\\ZipArchive' );
		$checks[] = [
			'id'          => 'zip_ext',
			'category'    => 'core',
			'label'       => 'ZipArchive extension',
			'value'       => $has_zip ? 'available' : 'missing',
			'requirement' => 'required, used to build archives',
			'status'      => $has_zip ? 'pass' : 'fail',
			'help'        => $has_zip ? '' : 'Ask your host to enable php-zip. Without it, archive creation cannot work.',
		];

		$has_ssl = extension_loaded( 'openssl' );
		$checks[] = [
			'id'          => 'openssl_ext',
			'category'    => 'core',
			'label'       => 'OpenSSL extension',
			'value'       => $has_ssl ? 'available' : 'missing',
			'requirement' => 'required, checksums + encryption',
			'status'      => $has_ssl ? 'pass' : 'fail',
		];

		$has_curl = extension_loaded( 'curl' );
		$checks[] = [
			'id'          => 'curl_ext',
			'category'    => 'core',
			'label'       => 'cURL extension',
			'value'       => $has_curl ? 'available' : 'missing',
			'requirement' => 'recommended, cloud destinations + notifications',
			'status'      => $has_curl ? 'pass' : 'warn',
		];

		/* ─── PHP runtime config ──────────────────────────────────────── */

		$mem_str  = (string) ini_get( 'memory_limit' );
		$mem      = $this->parse_bytes( $mem_str );
		$checks[] = [
			'id'          => 'memory_limit',
			'category'    => 'php',
			'label'       => 'PHP memory limit',
			'value'       => $mem_str,
			'requirement' => '256M minimum, 512M recommended',
			'status'      => $mem >= 512 * 1048576 ? 'pass'
				: ( $mem >= 256 * 1048576 ? 'warn' : 'fail' ),
			'help'        => $mem < 256 * 1048576 ? 'Large sites (>200MB) need more memory for the archive step. Bump memory_limit in php.ini.' : '',
		];

		$exec     = (int) ini_get( 'max_execution_time' );
		$exec_str = 0 === $exec ? 'unlimited' : "{$exec}s";
		$checks[] = [
			'id'          => 'max_execution_time',
			'category'    => 'php',
			'label'       => 'Max execution time',
			'value'       => $exec_str,
			'requirement' => '300s minimum (or unlimited via CLI/cron)',
			'status'      => 0 === $exec ? 'pass'
				: ( $exec >= 300 ? 'pass' : ( $exec >= 60 ? 'warn' : 'fail' ) ),
			'help'        => $exec > 0 && $exec < 300 ? 'WP-Cron-triggered backups still get this budget. Long backups may time out.' : '',
		];

		$upload   = $this->parse_bytes( ini_get( 'upload_max_filesize' ) );
		$post     = $this->parse_bytes( ini_get( 'post_max_size' ) );
		$import   = min( $upload, $post );
		$checks[] = [
			'id'          => 'upload_size',
			'category'    => 'php',
			'label'       => 'Max upload size (for Import)',
			'value'       => size_format( $import ),
			'requirement' => '100M recommended',
			'status'      => $import >= 100 * 1048576 ? 'pass' : 'warn',
			'help'        => 'Limited by min(upload_max_filesize, post_max_size). The chunked import sends ~0.7 MB pieces, so this isn\'t a real ceiling.',
		];

		/* ─── WAF exemption for chunked-import endpoint ──────────────── */

		$server = \Backreload\Core\WafExemption::server_type();
		if ( \Backreload\Core\WafExemption::uses_htaccess() ) {
			$installed = \Backreload\Core\WafExemption::is_installed();
			$checks[]  = [
				'id'          => 'waf_exemption',
				'category'    => 'wordpress',
				'label'       => 'Import WAF exemption (.htaccess)',
				'value'       => $installed ? 'Installed' : 'Missing (.htaccess not writable)',
				'requirement' => 'Auto-installed on ' . ucfirst( $server ),
				'status'      => $installed ? 'pass' : 'warn',
				'help'        => $installed
					? 'Disables LiteSpeed / mod_security rules for /wp-json/backreload/v1/backups/import only, which prevents the HTTP 406 on plugin/theme zip uploads.'
					: 'Make the site\'s .htaccess writable, deactivate and reactivate the plugin, OR paste the snippet from the docs into .htaccess manually.',
			];
		} elseif ( 'nginx' === $server ) {
			$checks[] = [
				'id'          => 'waf_exemption',
				'category'    => 'wordpress',
				'label'       => 'Import WAF exemption (nginx)',
				'value'       => 'Manual config required',
				'requirement' => 'nginx doesn\'t read .htaccess',
				'status'      => 'warn',
				'help'        => "Add to your server block to prevent ModSecurity from blocking the chunked-import endpoint:\n\n" . \Backreload\Core\WafExemption::nginx_snippet(),
			];
		}

		/* ─── Filesystem ──────────────────────────────────────────────── */

		$dir      = \Backreload\Storage::root();
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Read-only writability probe of a target path.
		$writable = is_dir( $dir ) && is_writable( $dir );
		$checks[] = [
			'id'          => 'backup_dir',
			'category'    => 'filesystem',
			'label'       => 'Backup directory writable',
			'value'       => $dir,
			'requirement' => 'must be writable by the web server',
			'status'      => $writable ? 'pass' : 'fail',
			'help'        => $writable ? '' : 'Create the directory with permissions 755, owned by the web-server user.',
		];

		$free = @disk_free_space( $dir ?: wp_get_upload_dir()['basedir'] );
		if ( false !== $free && $free > 0 ) {
			$checks[] = [
				'id'          => 'disk_free',
				'category'    => 'filesystem',
				'label'       => 'Free disk space',
				'value'       => size_format( $free ),
				'requirement' => '2 GB minimum, 10 GB recommended',
				'status'      => $free >= 10 * 1073741824 ? 'pass'
					: ( $free >= 2 * 1073741824 ? 'warn' : 'fail' ),
			];
		}

		/* ─── WordPress integrations ─────────────────────────────────── */

		$cron_disabled = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
		$checks[] = [
			'id'          => 'wp_cron',
			'category'    => 'wordpress',
			'label'       => 'WP-Cron',
			'value'       => $cron_disabled ? 'disabled' : 'enabled',
			'requirement' => 'enabled for scheduled backups',
			'status'      => $cron_disabled ? 'warn' : 'pass',
			'help'        => $cron_disabled
				? 'Schedules need either WP-Cron or a real OS-level cron hitting /wp-cron.php. "Backup now" still works (falls back to inline execution).'
				: '',
		];

		/* ─── Optional destination support ────────────────────────────── */

		$ftp_ok = function_exists( 'ftp_connect' );
		$checks[] = [
			'id'          => 'ftp_funcs',
			'category'    => 'optional',
			'label'       => 'FTP support',
			'value'       => $ftp_ok ? 'available' : 'missing',
			'requirement' => 'optional, FTP destination',
			'status'      => $ftp_ok ? 'pass' : 'warn',
		];

		$ssh2_ok = function_exists( 'ssh2_connect' );
		$checks[] = [
			'id'          => 'ssh2_ext',
			'category'    => 'optional',
			'label'       => 'SFTP support (PECL ssh2)',
			'value'       => $ssh2_ok ? 'available' : 'missing',
			'requirement' => 'optional, SFTP destination',
			'status'      => $ssh2_ok ? 'pass' : 'warn',
			'help'        => $ssh2_ok ? '' : 'Install php-ssh2 from your OS package manager, or run `composer require phpseclib/phpseclib` inside the plugin as a fallback.',
		];

		$aws_ok = class_exists( '\\Aws\\S3\\S3Client' );
		$checks[] = [
			'id'          => 'aws_sdk',
			'category'    => 'optional',
			'label'       => 'AWS SDK for PHP',
			'value'       => $aws_ok ? 'installed' : 'not installed',
			'requirement' => 'optional, AWS S3 / Wasabi / S3-compatible',
			'status'      => $aws_ok ? 'pass' : 'warn',
			'help'        => 'composer require aws/aws-sdk-php',
		];

		$google_ok = class_exists( '\\Google\\Client' );
		$checks[] = [
			'id'          => 'google_sdk',
			'category'    => 'optional',
			'label'       => 'Google API Client',
			'value'       => $google_ok ? 'installed' : 'not installed',
			'requirement' => 'optional, easier Drive integration (REST also works)',
			'status'      => $google_ok ? 'pass' : 'warn',
			'help'        => 'composer require google/apiclient',
		];

		return $checks;
	}

	private function parse_bytes( $value ): int {
		if ( function_exists( 'wp_convert_hr_to_bytes' ) ) {
			return (int) wp_convert_hr_to_bytes( $value );
		}
		$v = trim( (string) $value );
		$last = strtolower( substr( $v, -1 ) );
		$n = (int) $v;
		switch ( $last ) {
			case 'g': $n *= 1024;
			case 'm': $n *= 1024;
			case 'k': $n *= 1024;
		}
		return $n;
	}
}
