<?php
/**
 * Local environment diagnostics.
 *
 * @package LocalGuardScanner
 */

namespace LocalGuardScanner\Application\Diagnostics;

use LocalGuardScanner\Application\Compatibility;
use LocalGuardScanner\Application\Scan\ScanFailureCatalog;
use LocalGuardScanner\Infrastructure\Database\Schema;
use LocalGuardScanner\Infrastructure\WordPress\CronScheduler;

/**
 * Performs read-only checks without network requests.
 */
final class EnvironmentDiagnostics {
	/** @var \wpdb */
	private $wpdb;

	/** @param \wpdb $wpdb WordPress database connection. */
	public function __construct( $wpdb ) {
		$this->wpdb = $wpdb;
	}

	/**
	 * @param array<string,mixed> $settings Current settings.
	 * @return array<int,array{id:string,status:string,message_key:string,detail:string}>
	 */
	public function run( array $settings ) {
		global $wp_version;

		$checks   = array();
		$checks[] = $this->check(
			'compatibility',
			Compatibility::is_supported( PHP_VERSION, (string) $wp_version ) ? 'good' : 'error',
			Compatibility::is_supported( PHP_VERSION, (string) $wp_version ) ? 'environment_supported' : 'environment_unsupported',
			'WordPress ' . (string) $wp_version . ' · PHP ' . PHP_VERSION
		);
		$checks[] = $this->check(
			'php_recommendation',
			version_compare( PHP_VERSION, '8.3', '>=' ) ? 'good' : 'warning',
			version_compare( PHP_VERSION, '8.3', '>=' ) ? 'php_recommended' : 'php_upgrade_recommended',
			'PHP ' . PHP_VERSION
		);

		$memory_bytes  = self::memory_limit_bytes( (string) ini_get( 'memory_limit' ) );
		$memory_good   = -1 === $memory_bytes || $memory_bytes >= 128 * 1024 * 1024;
		$memory_detail = -1 === $memory_bytes ? __( 'Unlimited PHP memory limit', 'localguard-scanner' ) : size_format( $memory_bytes );
		if ( ! is_string( $memory_detail ) ) {
			$memory_detail = (string) $memory_bytes;
		}
		$checks[] = $this->check(
			'memory',
			$memory_good ? 'good' : 'warning',
			$memory_good ? 'memory_sufficient' : 'memory_constrained',
			$memory_detail
		);

		$cron_disabled   = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
		$daily_missing   = ! empty( $settings['daily_enabled'] ) && ! wp_next_scheduled( CronScheduler::DAILY_HOOK );
		$cron_good       = ! $cron_disabled && ! $daily_missing;
		$checks[]        = $this->check(
			'cron',
			$cron_good ? 'good' : 'warning',
			$cron_good ? 'cron_available' : ( $cron_disabled ? 'cron_disabled' : 'cron_event_missing' ),
			''
		);
		$last_cron_error = get_site_option( 'lgs_last_cron_error', array() );
		if ( is_array( $last_cron_error ) && isset( $last_cron_error['time'] ) && (int) $last_cron_error['time'] > time() - DAY_IN_SECONDS ) {
			$code     = isset( $last_cron_error['code'] ) ? ScanFailureCatalog::sanitize_code( $last_cron_error['code'] ) : '';
			$detail   = gmdate( 'Y-m-d H:i:s', (int) $last_cron_error['time'] ) . ' UTC' . ( '' !== $code ? ' · ' . $code : '' );
			$checks[] = $this->check( 'cron_failure', 'warning', 'cron_recent_failure', $detail );
		}

		$files_good = is_readable( ABSPATH ) && is_readable( LOCALGUARD_SCANNER_DIR . 'rules/manifest.json' ) && is_readable( LOCALGUARD_SCANNER_DIR . 'rules/manifest.sig' );
		$checks[]   = $this->check( 'files', $files_good ? 'good' : 'error', $files_good ? 'files_readable' : 'files_unreadable', '' );

		$tables_good = true;
		foreach ( Schema::table_names( $this->wpdb ) as $key => $table ) {
			$query = $this->wpdb->prepare( 'SHOW TABLES LIKE %s', $this->wpdb->esc_like( $table ) );
			if ( $table !== $this->wpdb->get_var( $query ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				$tables_good = false;
				break;
			}
			$columns_query = $this->wpdb->prepare( 'SHOW COLUMNS FROM %i', $table );
			$columns       = array_map( 'strval', $this->wpdb->get_col( $columns_query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$required      = Schema::required_columns();
			if ( ! isset( $required[ $key ] ) || array_diff( $required[ $key ], $columns ) ) {
				$tables_good = false;
				break;
			}
		}
		$checks[] = $this->check( 'database', $tables_good ? 'good' : 'error', $tables_good ? 'tables_available' : 'tables_missing', '' );

		return $checks;
	}

	/**
	 * Parse a PHP memory shorthand value.
	 *
	 * @param string $value Raw ini value.
	 * @return int -1 means unlimited.
	 */
	public static function memory_limit_bytes( $value ) {
		$value = trim( $value );
		if ( '-1' === $value ) {
			return -1;
		}
		if ( '' === $value ) {
			return 0;
		}
		$unit   = strtolower( substr( $value, -1 ) );
		$number = (float) $value;
		if ( 'g' === $unit ) {
			$number *= 1024;
			$unit    = 'm';
		}
		if ( 'm' === $unit ) {
			$number *= 1024;
			$unit    = 'k';
		}
		if ( 'k' === $unit ) {
			$number *= 1024;
		}
		return max( 0, (int) $number );
	}

	/**
	 * @return array{id:string,status:string,message_key:string,detail:string}
	 */
	private function check( string $id, string $status, string $message_key, string $detail ): array {
		return array(
			'id'          => $id,
			'status'      => $status,
			'message_key' => $message_key,
			'detail'      => $detail,
		);
	}
}
