<?php

/**
 * Parses LLM markdown responses into PHP / JS / CSS blocks.
 *
 * @package LolaCore\PluginBuilder\Generation
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\PluginBuilder\Generation;

use LolaCore\PluginBuilder\Playground\WooCommercePlaygroundBootstrap;

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

/**
 * Extracts fenced code blocks and metadata JSON from model output.
 *
 * Uses strict markdown fences first, then lenient substring fallbacks.
 */
class CodeExtractor {

	/**
	 * @return array{php: string, js: string, css: string, slug: string, dependencies: array<int, string>}
	 *
	 * @throws \Exception When any required code block is missing.
	 */
	/**
	 * @return array{php: string, js: string, css: string}
	 */
	public function extractBlocks( string $aiResponse ): array {
		$text = trim($aiResponse);

		return array(
			'php' => $this->extractPhpBlock($text),
			'js'  => $this->extractJsBlock($text),
			'css' => $this->extractCssBlock($text),
		);
	}

	/**
	 * @param array{php: string, js: string, css: string} $primary
	 * @param array{php: string, js: string, css: string} $secondary
	 * @return array{php: string, js: string, css: string}
	 */
	public function mergeBlocks( array $primary, array $secondary ): array {
		return array(
			'php' => $primary['php'] !== '' ? $primary['php'] : $secondary['php'],
			'js'  => $primary['js'] !== '' ? $primary['js'] : $secondary['js'],
			'css' => $primary['css'] !== '' ? $primary['css'] : $secondary['css'],
		);
	}

	/**
	 * @param array{php: string, js: string, css: string} $blocks
	 * @return array<int, string>
	 */
	public function missingBlockLabels( array $blocks ): array {
		$missing = array();
		if ($blocks['php'] === '') {
			$missing[] = 'PHP';
		}
		if ($blocks['js'] === '') {
			$missing[] = 'JavaScript';
		}
		if ($blocks['css'] === '') {
			$missing[] = 'CSS';
		}

		return $missing;
	}

	/**
	 * @param array{php: string, js: string, css: string} $blocks
	 */
	public function packBlocksAsMarkdown( array $blocks, string $metadataJson = '' ): string {
		$markdown  = "```php\n" . trim($blocks['php']) . "\n```\n";
		$markdown .= "```js\n" . trim($blocks['js']) . "\n```\n";
		$markdown .= "```css\n" . trim($blocks['css']) . "\n```\n";

		if ($metadataJson !== '') {
			$markdown .= "```json\n" . trim($metadataJson) . "\n```\n";
		}

		return $markdown;
	}

	public function extract( string $aiResponse ): array {
		$text   = trim($aiResponse);
		$blocks = $this->extractBlocks($text);
		$php    = $blocks['php'];
		$js     = $blocks['js'];
		$css    = $blocks['css'];

		if ($php === '' || $js === '' || $css === '') {
			$missing = array();
			if ($php === '') {
				$missing[] = 'PHP';
			}
			if ($js === '') {
				$missing[] = 'JavaScript';
			}
			if ($css === '') {
				$missing[] = 'CSS';
			}

			$hint = $this->buildIncompleteHint($text);

			throw new \Exception(
				esc_html(
					sprintf(
						/* translators: %s: comma-separated list of missing code block labels */
						__('Generated plugin is incomplete. Missing code blocks: %s.', 'lolacore'),
						implode(', ', $missing)
					)
					. ( $hint !== '' ? ' ' . $hint : '' )
				)
			);
		}

		$metadata = $this->extractMetadata($text);

		return array(
			'php'          => $php,
			'js'           => $js,
			'css'          => $css,
			'slug'         => $metadata['slug'],
			'dependencies' => $metadata['dependencies'],
		);
	}

	private function extractPhpBlock( string $text ): string {
		$fromFence = $this->extractFencedBlock($text, array( 'php' ));
		if ($fromFence !== '') {
			return $this->normalizePhpBlock($fromFence);
		}

		$fromTag = $this->extractBetweenMarkers($text, '<?php', '```');
		if ($fromTag !== '') {
			return $this->normalizePhpBlock($fromTag);
		}

		return '';
	}

	private function extractJsBlock( string $text ): string {
		$fromFence = $this->extractFencedBlock($text, array( 'javascript', 'js' ));
		if ($fromFence !== '') {
			return trim($fromFence);
		}

		foreach ( array( '```js', '```javascript' ) as $marker ) {
			$chunk = $this->extractAfterMarkerUntilFence($text, $marker);
			if ($chunk !== '') {
				return $chunk;
			}
		}

		return '';
	}

	private function extractCssBlock( string $text ): string {
		$fromFence = $this->extractFencedBlock($text, array( 'css' ));
		if ($fromFence !== '') {
			return trim($fromFence);
		}

		$chunk = $this->extractAfterMarkerUntilFence($text, '```css');
		if ($chunk !== '') {
			return $chunk;
		}

		return '';
	}

	/**
	 * @param array<int, string> $languages Fence language tags to try in order.
	 */
	private function extractFencedBlock( string $text, array $languages ): string {
		foreach ($languages as $language) {
			$quoted = preg_quote($language, '/');

			$patterns = array(
				'/```' . $quoted . '\s*\r?\n([\s\S]*?)\r?\n```/i',
				'/```' . $quoted . '\s*\r?\n([\s\S]*?)```/i',
				'/```' . $quoted . '\s+([\s\S]*?)```/i',
			);

			foreach ($patterns as $pattern) {
				if (preg_match($pattern, $text, $matches)) {
					return trim($matches[1]);
				}
			}
		}

		return '';
	}

	private function extractBetweenMarkers( string $text, string $startMarker, string $endMarker ): string {
		$start = strpos($text, $startMarker);
		if ($start === false) {
			return '';
		}

		$contentStart = $start;
		$searchFrom   = $start + strlen($startMarker);
		$end          = strpos($text, $endMarker, $searchFrom);

		if ($end === false) {
			return trim(substr($text, $contentStart));
		}

		return trim(substr($text, $contentStart, $end - $contentStart));
	}

	private function extractAfterMarkerUntilFence( string $text, string $marker ): string {
		$start = stripos($text, $marker);
		if ($start === false) {
			return '';
		}

		$contentStart = $start + strlen($marker);
		$content      = substr($text, $contentStart);
		$end          = strpos($content, '```');

		if ($end === false) {
			return trim($content);
		}

		return trim(substr($content, 0, $end));
	}

	private function normalizePhpBlock( string $php ): string {
		$php = trim($php);

		if ($php === '') {
			return '';
		}

		if (stripos($php, '<?php') !== 0) {
			$php = "<?php\n" . ltrim($php);
		}

		return $php;
	}

	private function buildIncompleteHint( string $text ): string {
		if ($text === '') {
			return __('The model returned an empty response. Check your model in LolaCore Settings and ensure the plugin code is in the main response text.', 'lolacore');
		}

		if (str_contains($text, '```php') || str_contains($text, '<?php')) {
			return __('The response may have been truncated before all three files were finished.', 'lolacore');
		}

		if (preg_match('/\b(I will|I\'ll|Let me)\b/i', $text) === 1 && ! str_contains($text, '```php')) {
			return __('The model replied with prose instead of the required fenced code blocks.', 'lolacore');
		}

		return __('Ensure the model returns ```php, ```js, and ```css fenced blocks. For large WooCommerce plugins, try DeepSeek V4 Pro or run the request again.', 'lolacore');
	}

	/**
	 * @return array{slug: string, dependencies: array<int, string>}
	 */
	private function extractMetadata( string $text ): array {
		$slug         = '';
		$dependencies = array();

		$jsonBlock = $this->extractFencedBlock($text, array( 'json' ));
		if ($jsonBlock !== '') {
			$parsed = json_decode($jsonBlock, true);
			if (is_array($parsed)) {
				return $this->normalizeMetadata($parsed);
			}
		}

		$fromMarker = $this->extractAfterMarkerUntilFence($text, '```json');
		if ($fromMarker !== '') {
			$parsed = json_decode($fromMarker, true);
			if (is_array($parsed)) {
				return $this->normalizeMetadata($parsed);
			}
		}

		if (preg_match('/\{[\s\S]*"dependencies"[\s\S]*\}/', $text, $matches)) {
			$parsed = json_decode($matches[0], true);
			if (is_array($parsed)) {
				return $this->normalizeMetadata($parsed);
			}
		}

		return array(
			'slug'         => $slug,
			'dependencies' => $dependencies,
		);
	}

	/**
	 * @param array<string, mixed> $parsed
	 * @return array{slug: string, dependencies: array<int, string>}
	 */
	private function normalizeMetadata( array $parsed ): array {
		$slug = '';
		if (isset($parsed['slug']) && is_scalar($parsed['slug'])) {
			$slug = sanitize_title((string) $parsed['slug']);
		}

		$dependencies = array();
		if (isset($parsed['dependencies']) && is_array($parsed['dependencies'])) {
			// Drop bundled core script handles (jquery, etc.) so they never get
			// stored and later turned into a 404'ing installPlugin blueprint step.
			$dependencies = WooCommercePlaygroundBootstrap::filterInstallableSlugs($parsed['dependencies']);
		}

		return array(
			'slug'         => $slug,
			'dependencies' => $dependencies,
		);
	}
}
