<?php
/**
 * Analyse statique PHP locale — PHPCompatibilityWP si vendor présent.
 *
 * @package PCC
 * @license GPL-2.0-or-later
 */

defined( 'ABSPATH' ) || exit;

class PCC_Static_Analyzer {

	/**
	 * @return array<string, string[]>
	 */
	public static function removed_functions() {
		return array(
			'7.0' => array(
				'mysql_connect', 'mysql_pconnect', 'mysql_select_db', 'mysql_query', 'mysql_fetch_array',
				'mysql_fetch_assoc', 'mysql_fetch_row', 'mysql_num_rows', 'mysql_affected_rows',
				'mysql_real_escape_string', 'mysql_close', 'mysql_error', 'mysql_errno',
				'ereg', 'eregi', 'ereg_replace', 'eregi_replace', 'split', 'spliti',
				'session_register', 'session_unregister', 'session_is_registered',
				'mcrypt_encrypt', 'mcrypt_decrypt',
			),
			'8.0' => array(
				'each', 'create_function', 'get_magic_quotes_gpc', 'get_magic_quotes_runtime',
				'set_magic_quotes_runtime', 'magic_quotes_runtime',
			),
			'8.1' => array(
				'utf8_encode', 'utf8_decode',
			),
			'8.2' => array(),
			'8.3' => array(),
			'8.4' => array(),
			'8.5' => array(),
		);
	}

	/**
	 * @return array<string, string[]>
	 */
	public static function deprecated_functions() {
		return array(
			'8.1' => array( 'strftime', 'gmstrftime' ),
			'8.2' => array( 'utf8_encode', 'utf8_decode' ),
			'8.4' => array( 'mb_strimwidth' ),
		);
	}

	public function functions_removed_up_to( $target_php ) {
		$target = self::normalize_version( $target_php );
		$all    = self::removed_functions();
		$out    = array();
		foreach ( $all as $version => $funcs ) {
			if ( version_compare( $version, $target, '<=' ) ) {
				$out = array_merge( $out, $funcs );
			}
		}
		return array_values( array_unique( $out ) );
	}

	public static function normalize_version( $version ) {
		$parts = explode( '.', (string) $version );
		return $parts[0] . '.' . ( $parts[1] ?? '0' );
	}

	/**
	 * @param string $file
	 * @param string $target_php
	 * @param string $baseline_php
	 * @return array<int, array{severity:string,file:string,line:int,message:string,code:string}>
	 */
	public function scan_file( $file, $target_php, $baseline_php = '' ) {
		if ( ! is_readable( $file ) || ! preg_match( '/\.php$/i', $file ) ) {
			return array();
		}

		$findings = $this->scan_file_phpcs( $file, $target_php, $baseline_php );
		if ( empty( $findings ) ) {
			$findings = $this->scan_file_tokens( $file, $target_php );
		}

		if ( $baseline_php && version_compare( self::normalize_version( $baseline_php ), self::normalize_version( $target_php ), '<' ) ) {
			$findings = $this->apply_delta( $findings, $file, $target_php, $baseline_php );
		}

		return self::sort_findings( $findings );
	}

	/**
	 * @param array<int, array<string,mixed>> $findings
	 * @return array<int, array<string,mixed>>
	 */
	public static function sort_findings( $findings ) {
		usort(
			$findings,
			function ( $a, $b ) {
				$rank = array( 'error' => 0, 'warning' => 1 );
				$ra   = $rank[ $a['severity'] ?? 'warning' ] ?? 2;
				$rb   = $rank[ $b['severity'] ?? 'warning' ] ?? 2;
				if ( $ra !== $rb ) {
					return $ra - $rb;
				}
				$fa = (string) ( $a['file'] ?? '' );
				$fb = (string) ( $b['file'] ?? '' );
				if ( $fa !== $fb ) {
					return strcmp( $fa, $fb );
				}
				return (int) ( $a['line'] ?? 0 ) - (int) ( $b['line'] ?? 0 );
			}
		);
		return $findings;
	}

	/**
	 * Tooltips / aide contextuelle pour codes d'erreur fréquents.
	 *
	 * @return array<string, string>
	 */
	public static function error_hints() {
		return array(
			'pcc.create_function'   => 'https://www.php.net/manual/en/function.create-function.php',
			'pcc.dollar_brace'      => 'https://www.php.net/manual/en/migration82.deprecated.php',
			'pcc.removed_function'  => 'https://www.php.net/manual/en/appendices.php',
			'pcc.deprecated'        => 'https://www.php.net/manual/en/appendices.php',
			'pcc.wp_deprecated'     => 'https://developer.wordpress.org/reference/functions/_deprecated_function/',
		);
	}

	private function scan_file_phpcs( $file, $target_php, $baseline_php ) {
		$autoload = PCC_DIR . 'vendor/autoload.php';
		if ( ! is_readable( $autoload ) ) {
			return array();
		}

		require_once $autoload;

		if ( ! class_exists( 'PHP_CodeSniffer\Runner' ) ) {
			return array();
		}

		$test_version = self::normalize_version( $target_php );
		if ( $baseline_php ) {
			$base = self::normalize_version( $baseline_php );
			if ( version_compare( $base, $test_version, '<' ) ) {
				$test_version = $base . '-' . $test_version;
			}
		}

		$ruleset = PCC_DIR . 'phpcs-ruleset.xml';
		if ( ! is_readable( $ruleset ) ) {
			return array();
		}

		$upload = wp_upload_dir();
		$report = trailingslashit( $upload['basedir'] ) . 'pcc-phpcs-' . wp_generate_password( 8, false ) . '.json';

		$argv = array(
			'phpcs',
			'--standard=' . $ruleset,
			'--extensions=php',
			'--report=json',
			'--report-file=' . $report,
			'--runtime-set',
			'testVersion',
			$test_version,
			$file,
		);

		$_SERVER['argv'] = $argv;
		$_SERVER['argc'] = count( $argv );

		try {
			$runner = new PHP_CodeSniffer\Runner();
			$runner->runPHPCS();
		} catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
			if ( is_readable( $report ) ) {
				wp_delete_file( $report );
			}
			return array();
		}

		if ( ! is_readable( $report ) ) {
			return array();
		}

		$json = json_decode( file_get_contents( $report ), true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		wp_delete_file( $report );

		if ( ! is_array( $json ) || empty( $json['files'] ) ) {
			return array();
		}

		$findings = array();
		foreach ( $json['files'] as $path => $data ) {
			if ( empty( $data['messages'] ) ) {
				continue;
			}
			foreach ( $data['messages'] as $msg ) {
				$type       = (int) ( $msg['type'] ?? 0 );
				$findings[] = array(
					'severity' => 1 === $type ? 'error' : 'warning',
					'file'     => wp_normalize_path( $path ),
					'line'     => (int) ( $msg['line'] ?? 0 ),
					'message'  => (string) ( $msg['message'] ?? '' ),
					'code'     => (string) ( $msg['source'] ?? 'phpcs' ),
				);
			}
		}

		return $findings;
	}

	/**
	 * @param string $file
	 * @param string $target_php
	 * @return array<int, array{severity:string,file:string,line:int,message:string,code:string}>
	 */
	private function scan_file_tokens( $file, $target_php ) {
		$code = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		if ( false === $code || '' === $code ) {
			return array();
		}

		$findings = array();
		$removed  = array_flip( $this->functions_removed_up_to( $target_php ) );
		$target_n = self::normalize_version( $target_php );

		if ( function_exists( 'token_get_all' ) ) {
			$tokens = @token_get_all( $code ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
			if ( false === $tokens ) {
				return array(
					array(
						'severity' => 'error',
						'file'     => $file,
						'line'     => 1,
						'message'  => __( 'PHP syntax could not be parsed.', 'php-compatibility-checker-by-volade' ),
						'code'     => 'pcc.syntax',
					),
				);
			}

			$line = 1;
			for ( $i = 0, $len = count( $tokens ); $i < $len; $i++ ) {
				$token = $tokens[ $i ];
				if ( is_array( $token ) ) {
					$line = (int) $token[2];
					if ( T_STRING === $token[0] && isset( $tokens[ $i + 1 ] ) && '(' === $tokens[ $i + 1 ] ) {
						$name = strtolower( $token[1] );
						if ( isset( $removed[ $name ] ) ) {
							$findings[] = array(
								'severity' => 'error',
								'file'     => $file,
								'line'     => $line,
								'message'  => sprintf(
									/* translators: 1: function name, 2: PHP version */
									__( 'Function %1$s is not available in PHP %2$s.', 'php-compatibility-checker-by-volade' ),
									$name,
									$target_php
								),
								'code'     => 'pcc.removed_function.' . $name,
							);
						}
					}
				}
			}
		}

		foreach ( self::deprecated_functions() as $ver => $funcs ) {
			if ( version_compare( $target_n, self::normalize_version( $ver ), '<' ) ) {
				continue;
			}
			foreach ( $funcs as $func ) {
				if ( preg_match_all( '/\b' . preg_quote( $func, '/' ) . '\s*\(/i', $code, $m, PREG_OFFSET_CAPTURE ) ) {
					foreach ( $m[0] as $match ) {
						$findings[] = array(
							'severity' => 'warning',
							'file'     => $file,
							'line'     => self::offset_to_line( $code, $match[1] ),
							'message'  => sprintf(
								/* translators: 1: function name, 2: PHP version */
								__( 'Function %1$s is deprecated in PHP %2$s.', 'php-compatibility-checker-by-volade' ),
								$func,
								$ver
							),
							'code'     => 'pcc.deprecated.' . $func,
						);
					}
				}
			}
		}

		if ( version_compare( $target_n, '8.0', '>=' ) && preg_match_all( '/\bcreate_function\s*\(/i', $code, $m, PREG_OFFSET_CAPTURE ) ) {
			foreach ( $m[0] as $match ) {
				$findings[] = array(
					'severity' => 'error',
					'file'     => $file,
					'line'     => self::offset_to_line( $code, $match[1] ),
					'message'  => __( 'The create_function helper is no longer available in PHP 8.0.', 'php-compatibility-checker-by-volade' ),
					'code'     => 'pcc.create_function',
				);
			}
		}

		if ( version_compare( $target_n, '8.2', '>=' ) && preg_match_all( '/\$\{[a-zA-Z_]/', $code, $m, PREG_OFFSET_CAPTURE ) ) {
			foreach ( $m[0] as $match ) {
				$findings[] = array(
					'severity' => 'warning',
					'file'     => $file,
					'line'     => self::offset_to_line( $code, $match[1] ),
					'message'  => __( 'Deprecated dollar-brace string interpolation — use curly-brace syntax instead (PHP 8.2+).', 'php-compatibility-checker-by-volade' ),
					'code'     => 'pcc.dollar_brace',
				);
			}
		}

		return $findings;
	}

	/**
	 * Retire les findings déjà présents à la version baseline (mode delta).
	 *
	 * @param array  $findings
	 * @param string $file
	 * @param string $target_php
	 * @param string $baseline_php
	 * @return array
	 */
	private function apply_delta( $findings, $file, $target_php, $baseline_php ) {
		$baseline_findings = $this->scan_file_tokens( $file, $baseline_php );
		$baseline_keys     = array();
		foreach ( $baseline_findings as $bf ) {
			$baseline_keys[ ( $bf['code'] ?? '' ) . ':' . (int) ( $bf['line'] ?? 0 ) ] = true;
		}

		$out = array();
		foreach ( $findings as $f ) {
			$key = ( $f['code'] ?? '' ) . ':' . (int) ( $f['line'] ?? 0 );
			if ( isset( $baseline_keys[ $key ] ) ) {
				continue;
			}
			$out[] = $f;
		}
		return $out;
	}

	private static function offset_to_line( $code, $offset ) {
		return substr_count( substr( $code, 0, (int) $offset ), "\n" ) + 1;
	}

	/**
	 * @param string $dir
	 * @param bool   $skip_vendor
	 * @param array  $custom_excludes
	 * @return array{files:string[],skipped:int}
	 */
	public function list_php_files( $dir, $skip_vendor = true, $custom_excludes = array() ) {
		$dir = wp_normalize_path( $dir );
		if ( ! is_dir( $dir ) ) {
			return array(
				'files'   => array(),
				'skipped' => 0,
			);
		}

		$files   = array();
		$skipped = 0;
		$iter    = new RecursiveIteratorIterator(
			new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS )
		);

		foreach ( $iter as $file ) {
			if ( ! $file->isFile() ) {
				continue;
			}
			$path = wp_normalize_path( $file->getPathname() );
			if ( ! preg_match( '/\.php$/i', $path ) ) {
				continue;
			}
			if ( $skip_vendor && preg_match( '#/(vendor|node_modules|tests?|languages)/#i', $path ) ) {
				$skipped++;
				continue;
			}
			if ( $this->path_is_excluded( $path, $custom_excludes ) ) {
				$skipped++;
				continue;
			}
			$files[] = $path;
		}

		sort( $files );
		return array(
			'files'   => $files,
			'skipped' => $skipped,
		);
	}

	/**
	 * @param string   $path
	 * @param string[] $patterns
	 */
	private function path_is_excluded( $path, $patterns ) {
		foreach ( (array) $patterns as $pattern ) {
			$pattern = trim( (string) $pattern );
			if ( '' === $pattern ) {
				continue;
			}
			if ( false !== strpos( $path, wp_normalize_path( $pattern ) ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @param string $file
	 * @param string $target_wp
	 * @return array<int, array{severity:string,file:string,line:int,message:string,code:string}>
	 */
	public function scan_file_wp( $file, $target_wp ) {
		return PCC_Wp_Deprecations::scan_file( $file, $target_wp );
	}
}
