<?php

/**
 * Parses JSON objects from LLM text responses (prompt JSON mode).
 *
 * @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.

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

/**
 * Normalizes common LLM JSON output quirks before decoding.
 */
final class StructuredJsonParser {

	/**
	 * @return array<string, mixed>
	 *
	 * @throws \Exception When the payload cannot be decoded as a JSON object.
	 */
	public static function parseObject( string $text ): array {
		$cleaned = self::stripMarkdownFences(trim($text));

		if ($cleaned === '') {
			throw new \Exception(__('Plugin code review returned empty JSON.', 'lolacore'));
		}

		$decoded = self::tryDecode($cleaned);
		if ($decoded !== null) {
			return $decoded;
		}

		$extracted = self::extractJsonObject($cleaned);
		if ($extracted !== null) {
			$decoded = self::tryDecode($extracted);
			if ($decoded !== null) {
				return $decoded;
			}

			$decoded = self::tryDecode(self::repairTrailingCommas($extracted));
			if ($decoded !== null) {
				return $decoded;
			}
		}

		$decoded = self::tryDecode(self::repairTrailingCommas($cleaned));
		if ($decoded !== null) {
			return $decoded;
		}

		$fallback = self::extractReviewerFields($cleaned);
		if ($fallback !== null) {
			return $fallback;
		}

		throw new \Exception(__('Plugin code review returned invalid JSON.', 'lolacore'));
	}

	private static function stripMarkdownFences( string $text ): string {
		$cleaned = preg_replace('/^```(?:json)?\s*/i', '', $text) ?? $text;
		$cleaned = preg_replace('/\s*```\s*$/', '', $cleaned) ?? $cleaned;

		return trim($cleaned);
	}

	/**
	 * @return array<string, mixed>|null
	 */
	private static function tryDecode( string $json ): ?array {
		$decoded = json_decode($json, true);

		return is_array($decoded) ? $decoded : null;
	}

	private static function extractJsonObject( string $text ): ?string {
		$start = strpos($text, '{');
		if ($start === false) {
			return null;
		}

		$length   = strlen($text);
		$depth    = 0;
		$inString = false;
		$escape   = false;

		for ($i = $start; $i < $length; $i++) {
			$char = $text[ $i ];

			if ($escape) {
				$escape = false;
				continue;
			}

			if ($char === '\\' && $inString) {
				$escape = true;
				continue;
			}

			if ($char === '"') {
				$inString = ! $inString;
				continue;
			}

			if ($inString) {
				continue;
			}

			if ($char === '{') {
				++$depth;
				continue;
			}

			if ($char === '}') {
				--$depth;
				if ($depth === 0) {
					return substr($text, $start, $i - $start + 1);
				}
			}
		}

		return null;
	}

	private static function repairTrailingCommas( string $json ): string {
		$repaired = $json;

		do {
			$next = preg_replace('/,\s*([}\]])/', '$1', $repaired);
			if (! is_string($next) || $next === $repaired) {
				break;
			}
			$repaired = $next;
		} while (true);

		return $repaired;
	}

	/**
	 * Last-resort extraction for the reviewer JSON schema when decode fails.
	 *
	 * @return array<string, mixed>|null
	 */
	private static function extractReviewerFields( string $text ): ?array {
		if (! preg_match('/"confidence_level"\s*:\s*(\d+)/', $text, $confidenceMatch)) {
			return null;
		}

		$explanation = '';
		if (preg_match('/"explanation"\s*:\s*"((?:[^"\\\\]|\\\\.)*)"/s', $text, $explanationMatch)) {
			$explanation = stripcslashes($explanationMatch[1]);
		}

		$suggestions = self::extractSuggestionsField($text);

		return array(
			'confidence_level' => (int) $confidenceMatch[1],
			'explanation'      => $explanation,
			'suggestions'      => $suggestions,
		);
	}

	/**
	 * @return array<int, string>
	 */
	private static function extractSuggestionsField( string $text ): array {
		if (preg_match('/"suggestions"\s*:\s*\[\s*\]/', $text)) {
			return array();
		}

		if (! preg_match('/"suggestions"\s*:\s*\[(.*?)\]/s', $text, $suggestionsMatch)) {
			return array();
		}

		if (! preg_match_all('/"((?:[^"\\\\]|\\\\.)*)"/', $suggestionsMatch[1], $stringMatches)) {
			return array();
		}

		$suggestions = array();
		foreach ($stringMatches[1] as $item) {
			$suggestions[] = stripcslashes($item);
		}

		return $suggestions;
	}
}

