<?php

/**
 * Builds WordPress Playground blueprint URLs for generated plugins.
 *
 * @package LolaCore\PluginBuilder\Playground
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\PluginBuilder\Playground;

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

use LolaCore\Builder\Artifact\FilesMap;
use LolaCore\Builder\Playground\BlueprintBuilder;
use LolaCore\PluginBuilder\Packaging\PluginFileNormalizer;
use LolaCore\PluginBuilder\PostType\PluginPostType;

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

/**
 * Converts library entries into Playground blueprint URLs.
 *
 * The blueprint shell and artifact steps come from the shared BlueprintBuilder
 * (FS-S1 backbone); this class owns the Plugin Builder specifics: library
 * payload loading, disk normalizers, WooCommerce dependency handling, and the
 * hosted-blueprint REST URL.
 */
class BlueprintGenerator {

	public const PLAYGROUND_BASE_URL = BlueprintBuilder::PLAYGROUND_BASE_URL;

	public const PLAYGROUND_BLUEPRINT_URL_BASE = BlueprintBuilder::PLAYGROUND_BLUEPRINT_URL_BASE;

	private BlueprintBuilder $blueprintBuilder;

	public function __construct( ?BlueprintBuilder $blueprintBuilder = null ) {
		$this->blueprintBuilder = $blueprintBuilder ?? new BlueprintBuilder();
	}

	/**
	 * Build a Playground URL for the given library entry.
	 *
	 * @throws \Exception When the entry is invalid or the blueprint is too large.
	 */
	public function build( int $postId ): string {
		return $this->buildShareableUrl($postId);
	}

	/**
	 * Short Playground URL via hosted blueprint (safe to share in chat and MCP).
	 *
	 * @throws \Exception When the entry is invalid.
	 */
	public function buildShareableUrl( int $postId ): string {
		$this->loadPluginPayload($postId);

		$hostedUrl = $this->buildHostedBlueprintUrl($postId);

		return self::PLAYGROUND_BLUEPRINT_URL_BASE . rawurlencode($hostedUrl);
	}

	/**
	 * Long hash URL with embedded blueprint JSON (legacy / offline use).
	 *
	 * @throws \Exception When the entry is invalid or the blueprint is too large.
	 */
	public function buildHashUrl( int $postId ): string {
		$payload   = $this->loadPluginPayload($postId);
		$blueprint = $this->buildBlueprint($payload);

		return $this->blueprintBuilder->encodeHashUrl($blueprint);
	}

	/**
	 * Public REST URL that serves the blueprint JSON for Playground's blueprint-url param.
	 *
	 * @throws \Exception When the entry is invalid.
	 */
	public function buildHostedBlueprintUrl( int $postId ): string {
		$payload = $this->loadPluginPayload($postId);
		$token   = PlaygroundAccessToken::forPost($postId);

		$hostedUrl = rest_url('lolacore/v1/plugin-builder/playground-blueprint/' . $postId);
		if (! is_string($hostedUrl) || $hostedUrl === '') {
			throw new \Exception(__('Could not build the hosted Playground blueprint URL.', 'lolacore'));
		}

		return add_query_arg(
			array(
				'token' => $token,
			),
			$hostedUrl
		);
	}

	/**
	 * @return array<string, mixed> Blueprint array ready for JSON encoding.
	 */
	public function buildBlueprint( array $payload ): array {
		$slug = sanitize_title((string) ($payload['slug'] ?? ''));
		if ($slug === '') {
			throw new \Exception(__('Built plugin slug is missing.', 'lolacore'));
		}

		// Enforce the same disk-write contracts as the local install path so the
		// sandbox runs the asset names and header version the user will actually get.
		$version = max(1, (int) ($payload['version'] ?? 1));

		$php = $this->stripRequiresPluginsHeader(
			BlueprintBuilder::sanitizeFileContent(
				PluginFileNormalizer::normalizePhp((string) ($payload['php'] ?? ''), $slug, $version)
			)
		);
		$js  = BlueprintBuilder::sanitizeFileContent((string) ($payload['js'] ?? ''));
		$css = BlueprintBuilder::sanitizeFileContent((string) ($payload['css'] ?? ''));

		// Only PHP is mandatory. A plugin written directly via MCP is often PHP-only
		// (no js/css), and the install path already tolerates that; the blueprint
		// must too, or such a plugin could never be tested in Playground.
		if ($php === '') {
			throw new \Exception(__('Built plugin PHP is missing.', 'lolacore'));
		}

		$dependencies = is_array($payload['dependencies'] ?? null)
			? WooCommercePlaygroundBootstrap::normalizeDependencies(
				array_values(array_filter(array_map('strval', $payload['dependencies']))),
				$php
			)
			: WooCommercePlaygroundBootstrap::normalizeDependencies(array(), $php);

		$dependencySteps = array();
		foreach ($dependencies as $depSlug) {
			$dependencySteps[] = $this->blueprintBuilder->wordpressOrgPluginStep($depSlug);

			if ($depSlug === WooCommercePlaygroundBootstrap::DEPENDENCY_SLUG) {
				foreach (WooCommercePlaygroundBootstrap::sandboxSteps() as $wcStep) {
					$dependencySteps[] = $wcStep;
				}
			}
		}

		$files = array( "{$slug}.php" => $php );
		if ($js !== '') {
			$files["{$slug}.js"] = $js;
		}
		if ($css !== '') {
			$files["{$slug}.css"] = $css;
		}

		return $this->blueprintBuilder->buildFor(
			$slug,
			FilesMap::fromArray($files),
			$dependencySteps,
			WooCommercePlaygroundBootstrap::preferredLandingPage($dependencies, $php)
		);
	}

	/**
	 * @return array{
	 *     post_id: int,
	 *     name: string,
	 *     slug: string,
	 *     version: int,
	 *     php: string,
	 *     js: string,
	 *     css: string,
	 *     dependencies: array<int, string>
	 * }
	 *
	 * @throws \Exception When the library entry is missing or invalid.
	 */
	public function loadPluginPayload( int $postId ): array {
		$post = get_post($postId);
		if (! $post instanceof \WP_Post || $post->post_type !== PluginPostType::POST_TYPE) {
			throw new \Exception(__('Built plugin not found.', 'lolacore'));
		}

		$slug = (string) get_post_meta($postId, PluginPostType::META_SLUG, true);
		if ($slug === '') {
			throw new \Exception(__('Built plugin slug is missing.', 'lolacore'));
		}

		$dependencies = get_post_meta($postId, PluginPostType::META_DEPENDENCIES, true);
		if (! is_array($dependencies)) {
			$dependencies = array();
		}

		return array(
			'post_id'      => $postId,
			'name'         => $post->post_title,
			'slug'         => $slug,
			'version'      => max(1, (int) get_post_meta($postId, PluginPostType::META_VERSION, true)),
			'php'          => (string) get_post_meta($postId, PluginPostType::META_PHP, true),
			'js'           => (string) get_post_meta($postId, PluginPostType::META_JS, true),
			'css'          => (string) get_post_meta($postId, PluginPostType::META_CSS, true),
			'dependencies' => array_values(array_filter(array_map('strval', $dependencies))),
		);
	}

	/**
	 * Strip the `Requires Plugins` header from the PHP plugin file.
	 *
	 * In Playground, dependencies are installed via explicit blueprint steps.
	 * The header only triggers WP's native WP_Plugin_Dependencies validation,
	 * which can reject activation in the WASM environment even when the
	 * dependency was installed in a prior step (exit code 255, empty stderr).
	 */
	private function stripRequiresPluginsHeader( string $php ): string {
		return (string) preg_replace(
			'/^\s*\*\s*Requires Plugins:.*$/mi',
			'',
			$php
		);
	}
}

