<?php

/**
 * Generates three-file WordPress plugins via the WP AI Client API.
 *
 * @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;
}

/**
 * Calls the configured LLM to produce PHP, JS, and CSS source files.
 */
class PluginGenerator {

	private const CONTINUATION_SYSTEM = 'You complete truncated WordPress plugin generations. Output ONLY the missing fenced code blocks requested in the user message. No prose, no PHP unless explicitly asked.';

	private PromptLoader $promptLoader;
	private AiClientHelper $ai;
	private CodeExtractor $extractor;

	public function __construct(
		?PromptLoader $promptLoader = null,
		?AiClientHelper $ai = null,
		?CodeExtractor $extractor = null
	) {
		$this->promptLoader = $promptLoader ?? new PromptLoader();
		$this->ai           = $ai ?? new AiClientHelper();
		$this->extractor    = $extractor ?? new CodeExtractor();
	}

	/**
	 * @param array<string, mixed> $siteContext Optional overrides merged into default site context.
	 * @param bool                 $includeSiteContext When false, omits site context (iterations already embed full code).
	 * @param string               $prd Approved design contract (PRD). When non-empty, leads the prompt as the spec to satisfy.
	 * @return array{php: string, js: string, css: string, dependencies: array<int, string>, slug: string}
	 *
	 * @throws \Exception When generation or extraction fails.
	 */
	public function generate( string $description, array $siteContext = array(), bool $includeSiteContext = true, string $prd = '' ): array {
		$description = trim($description);
		if ($description === '') {
			throw new \Exception(__('A plugin description is required.', 'lolacore'));
		}

		$systemPrompt = $this->promptLoader->buildDeveloperSystemPrompt();
		$userMessage  = $this->buildUserMessage($description, $siteContext, $includeSiteContext, $prd);

		$maxTokens = $this->ai->generationMaxTokens();

		$response  = $this->ai->generateText(
			$systemPrompt,
			$userMessage,
			0.0,
			$maxTokens
		);

		$extracted = $this->extractWithContinuation($description, $response, $maxTokens);

		$slug = $extracted['slug'];
		if ($slug === '') {
			$slug = sanitize_title(wp_trim_words($description, 5, ''));
		}
		if ($slug === '') {
			$slug = 'custom-plugin';
		}

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

	/**
	 * Repair a failed artifact using a full repair prompt (fix-mode retry).
	 *
	 * @return array{php: string, js: string, css: string, dependencies: array<int, string>, slug: string}
	 *
	 * @throws \Exception When generation or extraction fails.
	 */
	public function generateRepair( string $repairPrompt ): array {
		$repairPrompt = trim($repairPrompt);
		if ($repairPrompt === '') {
			throw new \Exception(__('A repair prompt is required.', 'lolacore'));
		}

		$systemPrompt = $this->promptLoader->buildDeveloperSystemPrompt();
		$maxTokens    = $this->ai->generationMaxTokens();

		$response = $this->ai->generateText(
			$systemPrompt,
			$repairPrompt,
			0.0,
			$maxTokens
		);

		$extracted = $this->extractWithContinuation($repairPrompt, $response, $maxTokens);

		$slug = $extracted['slug'];
		if ($slug === '') {
			$slug = 'custom-plugin';
		}

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

	/**
	 * @return array{php: string, js: string, css: string, slug: string, dependencies: array<int, string>}
	 */
	private function extractWithContinuation( string $description, string $response, int $maxTokens ): array {
		$blocks = $this->extractor->extractBlocks($response);

		if ($this->extractor->missingBlockLabels($blocks) === array()) {
			return $this->extractor->extract($response);
		}

		if ($blocks['php'] === '') {
			return $this->extractor->extract($response);
		}

		$missing = $this->extractor->missingBlockLabels($blocks);
		$tail    = $this->ai->generateText(
			self::CONTINUATION_SYSTEM,
			$this->buildContinuationMessage($description, $blocks, $missing),
			0.0,
			$maxTokens
		);

		$merged         = $this->extractor->mergeBlocks($blocks, $this->extractor->extractBlocks($tail));
		$metadataJson   = $this->extractMetadataJsonFromResponses($response, $tail);

		return $this->extractor->extract(
			$this->extractor->packBlocksAsMarkdown($merged, $metadataJson)
		);
	}

	/**
	 * @param array{php: string, js: string, css: string} $blocks
	 * @param array<int, string>                         $missing
	 */
	private function buildContinuationMessage( string $description, array $blocks, array $missing ): string {
		$phpExcerpt = $this->truncateForContinuation($blocks['php'], 4000);

		return "The previous plugin generation was cut off before these files were finished: "
			. implode(', ', $missing)
			. ".\n\n"
			. "Functional requirements (unchanged):\n"
			. $description
			. "\n\n"
			. "PHP file already generated (do not repeat or rewrite PHP):\n"
			. $phpExcerpt
			. "\n\n"
			. 'Return ONLY the missing fenced blocks in this order: '
			. $this->continuationFenceOrder($missing)
			. "\nInclude a ```json metadata block with slug and dependencies when JavaScript is requested.";
	}

	/**
	 * @param array<int, string> $missing
	 */
	private function continuationFenceOrder( array $missing ): string {
		$order = array();
		if (in_array('JavaScript', $missing, true)) {
			$order[] = '```js';
		}
		if (in_array('CSS', $missing, true)) {
			$order[] = '```css';
		}

		return implode(', then ', $order) . '.';
	}

	private function truncateForContinuation( string $php, int $maxChars ): string {
		if (strlen($php) <= $maxChars) {
			return $php;
		}

		return '…' . substr($php, -$maxChars);
	}

	private function extractMetadataJsonFromResponses( string $primary, string $secondary ): string {
		foreach ( array( $secondary, $primary ) as $text ) {
			if (preg_match('/```json\s*\r?\n([\s\S]*?)\r?\n```/i', $text, $matches)) {
				return trim($matches[1]);
			}
		}

		return '{"slug":"","dependencies":[]}';
	}

	private function buildUserMessage( string $description, array $siteContext, bool $includeSiteContext, string $prd = '' ): string {
		// The approved PRD is the design contract: it already encodes the
		// architecture decisions Lola made from the site's structural facts
		// (e.g. "block theme → integrate via render_block"). It leads the
		// message so generation builds to satisfy it.
		$prd    = trim($prd);
		$prefix = $prd !== ''
			? "Approved design contract (PRD) — build the plugin to satisfy this:\n" . $prd . "\n\n"
			: '';

		if (! $includeSiteContext) {
			return $prefix . "Functional requirements:\n" . $description;
		}

		$context = array_merge($this->ai->buildDefaultSiteContext(), $siteContext);

		return $prefix
			. "Functional requirements:\n"
			. $description
			. "\n\nSite context (JSON):\n"
			. wp_json_encode($context, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
	}
}

