<?php

/**
 * Reviews generated plugin code and returns a confidence score.
 *
 * @package LolaCore\PluginBuilder\Generation
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\PluginBuilder\Generation;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Diagnostic exception messages; escaped at REST/chat presentation layer.

use LolaCore\PluginBuilder\Packaging\PluginFileNormalizer;

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

/**
 * Uses structured JSON output from the AI Client to score generated code.
 */
class CodeReviewer {

	/**
	 * Internal score when LLM review JSON cannot be parsed after silent retries.
	 * Must meet PluginBuilderHandler::MIN_CONFIDENCE_THRESHOLD (80).
	 */
	private const DEGRADED_REVIEW_CONFIDENCE = 80;

	/** Cap applied when the LLM note mentions fatal load-time issues despite a high score. */
	private const FATAL_NOTE_CONFIDENCE_CAP = 20;

	private PromptLoader $promptLoader;
	private AiClientHelper $ai;
	private RuntimeValidator $runtimeValidator;
	private PluginStaticSafetyGate $safetyGate;

	public function __construct(
		?PromptLoader $promptLoader = null,
		?AiClientHelper $ai = null,
		?RuntimeValidator $runtimeValidator = null,
		?PluginStaticSafetyGate $safetyGate = null
	) {
		$this->promptLoader     = $promptLoader ?? new PromptLoader();
		$this->ai               = $ai ?? new AiClientHelper();
		$this->runtimeValidator = $runtimeValidator ?? new RuntimeValidator();
		$this->safetyGate       = $safetyGate ?? new PluginStaticSafetyGate();
	}

	/**
	 * @param array{php?: string, js?: string, css?: string, slug?: string, description?: string, library_version?: int, dependencies?: array<int, string>} $code
	 * @param callable(int): void|null $onSilentParseRetry Invoked before each silent JSON parse retry (1-based).
	 * @return array{
	 *   confidence: int,
	 *   note: string,
	 *   suggestions: array<int, string>,
	 *   blocking?: bool,
	 *   _degraded_review?: bool,
	 *   safety_findings?: array<int, array{severity: string, code: string, message: string, line?: int}>,
	 *   load_safe?: array{status: string, message: string, line: int}
	 * }
	 *
	 * @throws \Exception When review fails or returns invalid data.
	 */
	public function review( array $code, ?callable $onSilentParseRetry = null ): array {
		$php = isset($code['php']) ? trim((string) $code['php']) : '';
		$js  = isset($code['js']) ? trim((string) $code['js']) : '';
		$css = isset($code['css']) ? trim((string) $code['css']) : '';

		if ($php === '' || $js === '' || $css === '') {
			throw new \Exception(__('Cannot review an incomplete plugin (PHP, JS, and CSS are required).', 'lolacore'));
		}

		$slug            = isset($code['slug']) ? (string) $code['slug'] : '';
		$libraryVersion  = max(1, (int) ( $code['library_version'] ?? 1 ));
		$dependencies    = isset($code['dependencies']) && is_array($code['dependencies'])
			? $code['dependencies']
			: array();

		$lintError = $this->lintPhp($php);
		if ($lintError !== null) {
			return array(
				'confidence'  => 0,
				'note'        => 'PHP Syntax Error detected. The code cannot run.',
				'suggestions' => array('Fix this PHP syntax error: ' . $lintError),
			);
		}

		$safety = $this->safetyGate->analyze(
			array(
				'php'          => $php,
				'slug'         => $slug,
				'dependencies' => $dependencies,
			)
		);
		if ($safety['blocking']) {
			return $this->buildBlockingReviewResponse($safety);
		}

		$phpForRuntime = $slug !== ''
			? PluginFileNormalizer::normalizePhp($php, $slug, $libraryVersion)
			: $php;

		// Real runtime validation: load the plugin in a WordPress sandbox to catch
		// fatals the parser accepts as valid syntax. Degrades to "skipped" when it
		// cannot run safely, so it never blocks a build on a false negative.
		$runtime = $this->runtimeValidator->validate($phpForRuntime, $slug !== '' ? $slug : 'plugin', $dependencies);
		if (($runtime['status'] ?? '') === RuntimeValidator::STATUS_FATAL) {
			$line    = isset($runtime['line']) ? (int) $runtime['line'] : 0;
			$message = isset($runtime['message']) ? (string) $runtime['message'] : 'The plugin failed to load.';

			return array(
				'confidence'      => 0,
				'note'            => 'The plugin triggers a fatal error when WordPress loads it, so it cannot run.',
				'suggestions'     => array(
					'Fix this fatal error' . ($line > 0 ? ' on line ' . $line : '') . ': ' . $message,
				),
				'blocking'        => false,
				'safety_findings' => $safety['findings'],
				'load_safe'       => self::loadSafeSnapshot($runtime),
			);
		}

		$systemPrompt = $this->promptLoader->buildReviewerSystemPrompt();
		$userMessage  = $this->buildReviewPayload($code);

		$schema = array(
			'type'       => 'object',
			'properties' => array(
				'confidence_level' => array(
					'type'    => 'integer',
					'minimum' => 0,
					'maximum' => 100,
				),
				'explanation'      => array(
					'type' => 'string',
				),
				'suggestions'      => array(
					'type'  => 'array',
					'items' => array(
						'type' => 'string',
					),
				),
			),
			'required'   => array( 'confidence_level', 'explanation', 'suggestions' ),
		);

		try {
			$result = $this->ai->generateJsonWithRetry(
				$systemPrompt,
				$userMessage,
				$schema,
				function ( int $retryAttempt ) use ( $onSilentParseRetry ): void {
					if ($onSilentParseRetry !== null) {
						$onSilentParseRetry($retryAttempt);
					}
				}
			);
		} catch ( \Exception $e ) {
			if (! AiClientHelper::isTransientReviewParseException($e)) {
				throw $e;
			}

			return $this->buildDegradedReviewResponse($safety, $runtime);
		}

		$confidence = isset($result['confidence_level']) ? (int) $result['confidence_level'] : 0;
		$confidence = max(0, min(100, $confidence));

		$note = isset($result['explanation']) ? trim((string) $result['explanation']) : '';

		$suggestions = array();
		if (isset($result['suggestions']) && is_array($result['suggestions'])) {
			foreach ($result['suggestions'] as $item) {
				if (! is_scalar($item)) {
					continue;
				}
				$text = trim((string) $item);
				if ($text !== '') {
					$suggestions[] = $text;
				}
				if (count($suggestions) >= 5) {
					break;
				}
			}
		}

		$confidence = $this->applyConfidenceAdjustments($confidence, $safety, $note);

		return array(
			'confidence'      => $confidence,
			'note'            => $note,
			'suggestions'     => $suggestions,
			'blocking'        => false,
			'safety_findings' => $safety['findings'],
			'load_safe'       => self::loadSafeSnapshot($runtime),
		);
	}

	/**
	 * @param array{
	 *   blocking: bool,
	 *   confidence_cap: ?int,
	 *   findings: array<int, array{severity: string, code: string, message: string, line?: int}>
	 * } $safety
	 * @param array{status?: string, message?: string, line?: int} $runtime
	 * @return array{
	 *   confidence: int,
	 *   note: string,
	 *   suggestions: array<int, string>,
	 *   _degraded_review: bool,
	 *   blocking: bool,
	 *   safety_findings: array<int, array{severity: string, code: string, message: string, line?: int}>,
	 *   load_safe: array{status: string, message: string, line: int}
	 * }
	 */
	private function buildDegradedReviewResponse( array $safety, array $runtime ): array {
		return array(
			'confidence'       => self::DEGRADED_REVIEW_CONFIDENCE,
			'note'             => '',
			'suggestions'      => array(),
			'_degraded_review' => true,
			'blocking'         => false,
			'safety_findings'  => $safety['findings'],
			'load_safe'        => self::loadSafeSnapshot($runtime),
		);
	}

	private function buildBlockingReviewResponse( array $safety ): array {
		$suggestions = array();
		foreach ($safety['findings'] as $finding) {
			$severity = (string) ( $finding['severity'] ?? '' );
			if ($severity !== 'fatal' && $severity !== 'error') {
				continue;
			}

			$message = trim((string) ( $finding['message'] ?? '' ));
			if ($message !== '') {
				$suggestions[] = $message;
			}
		}

		return array(
			'confidence'      => 0,
			'note'            => 'Internal safety checks found blocking issues that would cause a fatal error when the plugin loads.',
			'suggestions'     => $suggestions,
			'blocking'        => true,
			'safety_findings' => $safety['findings'],
			'load_safe'       => self::loadSafeSnapshot(array()),
		);
	}

	/**
	 * @param array{confidence_cap?: ?int} $safety
	 */
	private function applyConfidenceAdjustments( int $confidence, array $safety, string $note ): int {
		if (isset($safety['confidence_cap']) && $safety['confidence_cap'] !== null) {
			$confidence = min($confidence, (int) $safety['confidence_cap']);
		}

		return $this->applyFatalKeywordPostCap($confidence, $note);
	}

	private function applyFatalKeywordPostCap( int $confidence, string $note ): int {
		if ($note === '') {
			return $confidence;
		}

		$keywords = array(
			'fatal error',
			'fatal ',
			'would fatal',
			'class not found',
			'cannot run',
			'cannot load',
			'triggers a fatal',
			'parse error',
		);

		$lower = strtolower($note);
		foreach ($keywords as $keyword) {
			if (str_contains($lower, $keyword)) {
				return min($confidence, self::FATAL_NOTE_CONFIDENCE_CAP);
			}
		}

		return $confidence;
	}

	/**
	 * @param array{status?: string, message?: string, line?: int} $runtime
	 * @return array{status: string, message: string, line: int}
	 */
	private static function loadSafeSnapshot( array $runtime ): array {
		return array(
			'status'  => (string) ( $runtime['status'] ?? RuntimeValidator::STATUS_SKIPPED ),
			'message' => (string) ( $runtime['message'] ?? '' ),
			'line'    => (int) ( $runtime['line'] ?? 0 ),
		);
	}

	/**
	 * @param array<string, mixed> $code
	 */
	private function buildReviewPayload( array $code ): string {
		$description = isset($code['description']) ? trim((string) $code['description']) : '';
		$slug        = isset($code['slug']) ? sanitize_title((string) $code['slug']) : '';

		$parts = array();
		if ($description !== '') {
			$parts[] = "```functional_requirements\n{$description}\n```";
		}
		if ($slug !== '') {
			$parts[] = "Plugin slug: {$slug}";
		}

		$parts[] = '```php' . "\n" . (string) $code['php'] . "\n```";
		$parts[] = '```js' . "\n" . (string) $code['js'] . "\n```";
		$parts[] = '```css' . "\n" . (string) $code['css'] . "\n```";

		return implode("\n\n", $parts);
	}

	/**
	 * Validates PHP syntax in-process, without shelling out to a binary.
	 *
	 * token_get_all() with the TOKEN_PARSE flag runs the full parser and throws
	 * \ParseError / \CompileError on invalid syntax. This works in every SAPI
	 * (CLI, cgi-fcgi, FPM) and on hosts that disable exec(), unlike `php -l`:
	 * under FastCGI, PHP_BINARY resolves to php-cgi, which ignores the file
	 * argument and returns "No input file specified.", failing every plugin.
	 *
	 * @return string|null The syntax error message, or null if valid.
	 */
	private function lintPhp( string $php ): ?string {
		try {
			token_get_all($php, TOKEN_PARSE);
		} catch (\CompileError $e) {
			// \ParseError extends \CompileError; this covers both.
			return $e->getMessage();
		}

		return null;
	}

}


