<?php

/**
 * Background worker for async Plugin Builder generation.
 *
 * Executes the full LLM generation pipeline (generate → review → retry)
 * in a separate PHP process spawned via wp_remote_post(blocking=false).
 * Updates a transient-based job state so the frontend can poll progress.
 *
 * @package LolaCore\PluginBuilder\Async
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\PluginBuilder\Async;

use LolaCore\Chat\PendingActionStore;
use LolaCore\PluginBuilder\Generation\CodeReviewer;
use LolaCore\PluginBuilder\Generation\GenerationRetryOrchestrator;
use LolaCore\PluginBuilder\Generation\PluginBuilderGenerationCache;
use LolaCore\PluginBuilder\Generation\PluginGenerator;
use LolaCore\PluginBuilder\Generation\RetryLedger;
use LolaCore\PluginBuilder\Handler\PluginBuilderHandler;
use LolaCore\PluginBuilder\PostType\PluginPostType;

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

class BuilderWorker {

	/** HMAC salt suffix — distinct from extractFacts to prevent token reuse. */
	private const HMAC_SALT_SUFFIX = '|lolacore_pb_async_build';

	private JobManager $jobManager;
	private PluginGenerator $generator;
	private CodeReviewer $reviewer;

	/** @var array<string, mixed>|null Artifact kept when generate() succeeded but the job may still fail. */
	private ?array $pendingFailureArtifact = null;

	private int $pendingFailureLibraryVersion = 1;

	public function __construct(
		?JobManager $jobManager = null,
		?PluginGenerator $generator = null,
		?CodeReviewer $reviewer = null
	) {
		$this->jobManager = $jobManager ?? new JobManager();
		$this->generator  = $generator ?? new PluginGenerator();
		$this->reviewer   = $reviewer ?? new CodeReviewer();
	}

	/**
	 * Register the AJAX hooks for the background worker.
	 */
	public function register(): void {
		\add_action( 'wp_ajax_lolacore_pb_async_build', array( $this, 'handleAsyncBuild' ) );
		\add_action( 'wp_ajax_nopriv_lolacore_pb_async_build', array( $this, 'handleAsyncBuild' ) );
	}

	/**
	 * Dispatch a background build job via non-blocking loopback POST.
	 *
	 * @return bool True if the request was dispatched. False if wp_remote_post failed
	 *              (caller should fall back to synchronous execution).
	 */
	public static function dispatch( string $jobId ): bool {
		$url   = \admin_url( 'admin-ajax.php' );
		$token = self::generateToken( $jobId );

		$result = \wp_remote_post( $url, array(
			'timeout'   => 0.1,
			'blocking'  => false,
			'sslverify' => false,
			'body'      => array(
				'action' => 'lolacore_pb_async_build',
				'job_id' => $jobId,
				'token'  => $token,
			),
		) );

		return ! \is_wp_error( $result );
	}

	/**
	 * AJAX handler invoked by the loopback request.
	 *
	 * This runs in a separate PHP process. It authenticates via HMAC,
	 * configures PHP for long-running execution, and processes the job.
	 */
	public function handleAsyncBuild(): void {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Authenticated via HMAC token below.
		$jobId = isset( $_POST['job_id'] ) ? \sanitize_text_field( \wp_unslash( $_POST['job_id'] ) ) : '';
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Authenticated via HMAC token below.
		$token = isset( $_POST['token'] ) ? \sanitize_text_field( \wp_unslash( $_POST['token'] ) ) : '';

		if ( empty( $jobId ) || empty( $token ) ) {
			\wp_die( '', '', array( 'response' => 400 ) );
		}

		$expected = self::generateToken( $jobId );
		if ( ! \hash_equals( $expected, $token ) ) {
			\wp_die( '', '', array( 'response' => 403 ) );
		}

		$state = $this->jobManager->getState( $jobId );
		if ( $state === null ) {
			\wp_die( '', '', array( 'response' => 404 ) );
		}

		// Survive nginx/Apache proxy timeout and PHP time limit.
		ignore_user_abort( true );
		if ( function_exists( 'set_time_limit' ) ) {
			// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Background worker must outlive proxy timeouts.
			@set_time_limit( 0 );
		}

		// Detach from the loopback client connection before the long LLM work.
		//
		// dispatch() fires a non-blocking loopback (blocking=false, timeout=0.1),
		// so the client socket is gone ~0.1s in. On Apache/FPM, ignore_user_abort()
		// keeps this process alive; on LiteSpeed (lsphp) it does NOT — LiteSpeed
		// reaps the disconnected request ~10s in, killing generate() mid-flight
		// (clean exit, no exception → job stalls at 5% forever).
		//
		// finish_request() tells the SAPI the response is complete so it releases
		// the connection cleanly and lets the script keep running detached.
		// Unavailable on Local/Windows cgi-fcgi — the ignore_user_abort() above is
		// the universal fallback there. See project_local_windows_sapi_cgi_fcgi.
		$this->finishRequestEarly();

		$this->processJob( $jobId, $state );

		\wp_die( '', '', array( 'response' => 200 ) );
	}

	/**
	 * Close the response so the SAPI releases the (already-abandoned) loopback
	 * connection and lets this worker keep running detached.
	 *
	 * LiteSpeed exposes litespeed_finish_request(); PHP-FPM exposes
	 * fastcgi_finish_request(). Neither exists under Local/Windows cgi-fcgi, where
	 * ignore_user_abort() in the caller is the fallback.
	 */
	private function finishRequestEarly(): void {
		// Best-effort flush of any buffered output before detaching.
		if ( function_exists( 'litespeed_finish_request' ) ) {
			\litespeed_finish_request();
			return;
		}

		if ( function_exists( 'fastcgi_finish_request' ) ) {
			\fastcgi_finish_request();
		}
	}

	/**
	 * Run the full generation pipeline with progress updates.
	 *
	 * @param array<string, mixed> $state Job state from transient.
	 */
	private function processJob( string $jobId, array $state ): void {
		$action = (string) ( $state['action'] ?? '' );
		$params = (array) ( $state['params'] ?? array() );

		try {
			if ( $action === 'create_plugin' ) {
				$this->processCreatePlugin( $jobId, $params, $state );
			} elseif ( $action === 'update_plugin_code' ) {
				$this->processUpdatePlugin( $jobId, $params, $state );
			} else {
				$this->jobManager->fail( $jobId, 'Unknown action: ' . $action );
			}
		} catch ( \Throwable $e ) {
			$this->failJob( $jobId, $e->getMessage() );
		} finally {
			$this->clearGenerationArtifact();
		}
	}

	/**
	 * @param array<string, mixed> $params
	 * @param array<string, mixed> $state
	 */
	private function processCreatePlugin( string $jobId, array $params, array $state = array() ): void {
		$description   = isset( $params['description'] ) ? trim( (string) $params['description'] ) : '';
		$requestedName = isset( $params['name'] ) ? \sanitize_text_field( (string) $params['name'] ) : '';
		$prd           = isset( $params['prd'] ) ? trim( (string) $params['prd'] ) : '';

		if ( $description === '' ) {
			$this->jobManager->fail( $jobId, 'description is required.' );
			return;
		}

		$libraryVersion = (int) ( $state['library_version'] ?? 1 );
		$seedLedger     = $this->loadSeedLedger( $state );
		$fixModeOnly    = (string) ( $state['worker_mode'] ?? JobManager::WORKER_MODE_GENERATE ) === JobManager::WORKER_MODE_FIX_RETRY;

		$generated = $this->resolveGenerationStart( $jobId, $state, $description, true, $prd );
		if ( $generated === null ) {
			return;
		}

		$this->trackGenerationArtifact( $generated, $libraryVersion );

		$this->jobManager->updateProgress( $jobId, 40, 'The frontend team is building JavaScript and CSS…' );

		// --- Review ---
		$this->jobManager->updateProgress( $jobId, 60, 'QA is auditing security and syntax…' );

		$review = $this->runReviewWithProgress(
			$jobId,
			array_merge(
				$generated,
				array(
					'description'     => $description,
					'library_version' => $libraryVersion,
				)
			)
		);

		$this->jobManager->updateProgress( $jobId, 70, 'Review complete. Checking quality standards…' );

		// --- Auto-retry if needed ---
		list( $generated, $review, $retried, $ledger ) = $this->maybeRetryGeneration(
			$jobId,
			$description,
			$generated,
			$review,
			true,
			$prd,
			$libraryVersion,
			$seedLedger,
			$fixModeOnly
		);

		// --- Quality gate ---
		if ( PluginBuilderHandler::reviewNeedsRetry( $review ) ) {
			$this->failWithGenerationContext( $jobId, $generated, $review, $ledger, $libraryVersion );
			return;
		}

		// --- Build preview ---
		$this->jobManager->updateProgress( $jobId, 95, 'Packaging the preview…' );

		$title = $requestedName !== ''
			? $requestedName
			: $this->deriveTitleFromCode( $generated['php'], $generated['slug'] );

		$cacheParams = array( 'description' => $description );
		if ( $requestedName !== '' ) {
			$cacheParams['name'] = $requestedName;
		}

		$cacheKey = PluginBuilderGenerationCache::store(
			'create_plugin',
			$cacheParams,
			array(
				'generated' => $generated,
				'review'    => $review,
				'title'     => $title,
			)
		);

		$previewData = PluginBuilderHandler::enrichPreviewQuality(
			array(
				'preview'           => true,
				'name'              => $title,
				'slug'              => $generated['slug'],
				'confidence'        => $review['confidence'],
				'note'              => $review['note'],
				'suggestions'       => $review['suggestions'],
				'preview_cache_key' => $cacheKey,
				'message'           => __('Your starter plugin is ready. It should activate and show the settings you asked for — save it, try it in Playground, and tell me what to refine.', 'lolacore'),
			),
			$review
		);

		if ( $retried ) {
			$previewData['retried'] = true;
		}

		$previewData = $this->attachConfirmation(
			'create_plugin',
			'createPlugin',
			array( 'description' => $description, 'name' => $requestedName, 'prd' => $prd ),
			$cacheKey,
			$previewData
		);

		$this->jobManager->complete( $jobId, $previewData );
	}

	/**
	 * @param array<string, mixed> $params
	 * @param array<string, mixed> $state
	 */
	private function processUpdatePlugin( string $jobId, array $params, array $state = array() ): void {
		$postId      = isset( $params['post_id'] ) ? (int) $params['post_id'] : 0;
		$instruction = isset( $params['instruction'] ) ? trim( (string) $params['instruction'] ) : '';

		if ( $postId <= 0 || $instruction === '' ) {
			$this->jobManager->fail( $jobId, 'post_id and instruction are required.' );
			return;
		}

		$post = \get_post( $postId );
		if ( ! $post instanceof \WP_Post || $post->post_type !== PluginPostType::POST_TYPE ) {
			$this->jobManager->fail( $jobId, 'Built plugin not found.' );
			return;
		}

		$slug                 = (string) \get_post_meta( $postId, PluginPostType::META_SLUG, true );
		$previousDescription  = (string) \get_post_meta( $postId, PluginPostType::META_DESCRIPTION, true );
		$currentVersion       = max( 1, (int) \get_post_meta( $postId, PluginPostType::META_VERSION, true ) );
		$newVersion           = $currentVersion + 1;

		$previousSuggestions = \get_post_meta( $postId, PluginPostType::META_SUGGESTIONS, true );
		$iterationPrompt = PluginBuilderHandler::buildUpdatePrompt(
			$previousDescription,
			(string) \get_post_meta( $postId, PluginPostType::META_PHP, true ),
			(string) \get_post_meta( $postId, PluginPostType::META_JS, true ),
			(string) \get_post_meta( $postId, PluginPostType::META_CSS, true ),
			$instruction,
			array(
				'confidence'  => (int) \get_post_meta( $postId, PluginPostType::META_CONFIDENCE, true ),
				'note'        => (string) \get_post_meta( $postId, PluginPostType::META_CONFIDENCE_NOTE, true ),
				'suggestions' => is_array( $previousSuggestions ) ? $previousSuggestions : array(),
			),
			(string) \get_post_meta( $postId, PluginPostType::META_PRD, true )
		);

		$seedLedger  = $this->loadSeedLedger( $state );
		$fixModeOnly = (string) ( $state['worker_mode'] ?? JobManager::WORKER_MODE_GENERATE ) === JobManager::WORKER_MODE_FIX_RETRY;

		$generated = $this->resolveGenerationStart( $jobId, $state, $iterationPrompt, false );
		if ( $generated === null ) {
			return;
		}
		if ( $slug !== '' ) {
			$generated['slug'] = $slug;
		}

		$this->trackGenerationArtifact( $generated, $newVersion );

		$this->jobManager->updateProgress( $jobId, 40, 'The frontend team is building JavaScript and CSS…' );

		// --- Review ---
		$this->jobManager->updateProgress( $jobId, 60, 'QA is auditing security and syntax…' );

		$fullDescription = $previousDescription . "\nUpdate: " . $instruction;
		$review          = $this->runReviewWithProgress(
			$jobId,
			array_merge(
				$generated,
				array(
					'description'     => $fullDescription,
					'library_version' => $newVersion,
				)
			)
		);

		$this->jobManager->updateProgress( $jobId, 70, 'Review complete. Checking quality standards…' );

		// --- Auto-retry ---
		list( $generated, $review, $retried, $ledger ) = $this->maybeRetryGeneration(
			$jobId,
			$fullDescription,
			$generated,
			$review,
			false,
			'',
			$newVersion,
			$seedLedger,
			$fixModeOnly
		);
		if ( $slug !== '' ) {
			$generated['slug'] = $slug;
		}

		// --- Quality gate ---
		if ( PluginBuilderHandler::reviewNeedsRetry( $review ) ) {
			$this->failWithGenerationContext( $jobId, $generated, $review, $ledger, $newVersion );
			return;
		}

		// --- Build preview ---
		$this->jobManager->updateProgress( $jobId, 95, 'Packaging the preview…' );

		$cacheKey = PluginBuilderGenerationCache::store(
			'update_plugin_code',
			array( 'post_id' => $postId, 'instruction' => $instruction ),
			array(
				'generated'   => $generated,
				'review'      => $review,
				'new_version' => $newVersion,
			)
		);

		$previewData = PluginBuilderHandler::enrichPreviewQuality(
			array(
				'preview'           => true,
				'post_id'           => $postId,
				'name'              => $post->post_title,
				'slug'              => $slug,
				'next_version'      => $newVersion,
				'confidence'        => $review['confidence'],
				'confidence_note'   => $review['note'],
				'suggestions'       => $review['suggestions'],
				'preview_cache_key' => $cacheKey,
				'message'           => __('Your update is ready to test. Save it when you\'re happy, then tell me what still needs to change.', 'lolacore'),
			),
			$review
		);

		if ( $retried ) {
			$previewData['retried'] = true;
		}

		$previewData = $this->attachConfirmation(
			'update_plugin_code',
			'updatePluginCode',
			array( 'post_id' => $postId, 'instruction' => $instruction ),
			$cacheKey,
			$previewData
		);

		$this->jobManager->complete( $jobId, $previewData );
	}

	/**
	 * Register a pending confirmation for the completed preview and enrich the
	 * result so the frontend can render a real, approvable tool_confirmation.
	 *
	 * The pending action mirrors the synchronous chat flow (see
	 * Controller::storePendingBatchAction), so the existing approve handler
	 * (handleConfirmToolAction) executes it unchanged — re-running the tool
	 * non-dry-run, which consumes the generation cache and saves the plugin.
	 * session_id is left empty here; the approve request supplies the live one.
	 *
	 * @param array<string, mixed> $params      Tool params (must match the cache fingerprint).
	 * @param array<string, mixed> $previewData Completed preview payload.
	 * @return array<string, mixed>
	 */
	private function attachConfirmation(
		string $executorAction,
		string $displayTool,
		array $params,
		string $cacheKey,
		array $previewData
	): array {
		$actionId = \wp_generate_uuid4();
		$preview  = $this->buildPreviewMarkdown( $executorAction, $previewData );

		PendingActionStore::storeBatch( $actionId, '', array(
			array(
				'tool_name'         => $executorAction,
				'tool_display_name' => $displayTool,
				'params'            => $params,
				'tool_call_id'      => 'async_' . $actionId,
				'preview'           => $preview,
				'preview_cache_key' => $cacheKey,
				'preview_failed'    => false,
				// The original LLM tool_call was already resolved at dispatch time
				// ("Generation started…"). On approve, the result is saved as an
				// assistant message — not a tool message — so it does not become an
				// orphan tool reply that would 400 every later turn in the session.
				'async'             => true,
			),
		) );

		$previewData['action_id']    = $actionId;
		$previewData['tool_name']    = $displayTool;
		$previewData['preview']      = $preview;
		$previewData['card_variant'] = 'plugin_builder_delivery';

		return $previewData;
	}

	/**
	 * Build a readable markdown summary of the preview for the confirmation card.
	 *
	 * @param array<string, mixed> $previewData
	 */
	private function buildPreviewMarkdown( string $executorAction, array $previewData ): string {
		$header = $executorAction === 'update_plugin_code'
			? '**Your update is ready to test**'
			: '**Your starter plugin is ready**';
		$lines  = array( $header, '' );

		foreach ( array( 'name', 'slug', 'next_version' ) as $field ) {
			if ( isset( $previewData[ $field ] ) && is_scalar( $previewData[ $field ] ) ) {
				$lines[] = '- **' . $field . ':** ' . (string) $previewData[ $field ];
			}
		}

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

	/**
	 * Re-generate the plugin if confidence is below threshold.
	 *
	 * @param array{php: string, js: string, css: string, slug: string, dependencies: array<int, string>} $generated
	 * @param array{confidence: int, note: string, suggestions: array<int, string>} $review
	 * @return array{0: array, 1: array, 2: bool, 3: RetryLedger|null} [generated, review, retried, ledger]
	 */
	private function maybeRetryGeneration(
		string $jobId,
		string $description,
		array $generated,
		array $review,
		bool $includeSiteContext = true,
		string $prd = '',
		int $libraryVersion = 1,
		?RetryLedger $seedLedger = null,
		bool $fixModeOnly = false
	): array {
		if ( ! PluginBuilderHandler::reviewNeedsRetry( $review ) ) {
			return array( $generated, $review, false, null );
		}

		$this->jobManager->updateProgress( $jobId, 75, 'QA found improvements. The team is applying fixes…' );

		$orchestrator = new GenerationRetryOrchestrator( $this->generator, $this->reviewer );
		list( $generated, $review, $retried, $stuck, $ledger ) = $orchestrator->run(
			$description,
			$generated,
			$review,
			$includeSiteContext,
			$prd,
			null,
			$libraryVersion,
			$seedLedger,
			$fixModeOnly
		);

		if ( $stuck ) {
			$review['_retry_stuck'] = true;
		}

		$this->jobManager->updateProgress( $jobId, 85, 'Fixes applied. Running second review…' );

		return array( $generated, $review, $retried, $ledger );
	}

	/**
	 * Load a saved artifact for user fix_retry, or run a clean generate() fallback.
	 *
	 * @param array<string, mixed> $state
	 * @return array{php: string, js: string, css: string, slug: string, dependencies?: array<int, string>}|null
	 */
	private function resolveGenerationStart(
		string $jobId,
		array $state,
		string $prompt,
		bool $includeSiteContext = true,
		string $prd = ''
	): ?array {
		$workerMode = (string) ( $state['worker_mode'] ?? JobManager::WORKER_MODE_GENERATE );

		if ( $workerMode === JobManager::WORKER_MODE_FIX_RETRY ) {
			$artifact = $this->artifactFromState( $state );
			if ( $artifact !== null ) {
				$this->jobManager->updateProgress( $jobId, 5, 'Continuing from your last build…' );

				return $artifact;
			}
		}

		$this->jobManager->updateProgress( $jobId, 5, 'The architect is designing the plugin structure…' );
		$generated = $this->generator->generate( $prompt, array(), $includeSiteContext, $prd );

		return $generated;
	}

	/**
	 * @param array<string, mixed> $state
	 * @return array{php: string, js: string, css: string, slug: string, dependencies?: array<int, string>}|null
	 */
	private function artifactFromState( array $state ): ?array {
		$artifact = $state['last_artifact'] ?? null;
		if ( ! is_array( $artifact ) ) {
			return null;
		}

		$php = isset( $artifact['php'] ) ? (string) $artifact['php'] : '';
		$js  = isset( $artifact['js'] ) ? (string) $artifact['js'] : '';
		$css = isset( $artifact['css'] ) ? (string) $artifact['css'] : '';
		$slug = isset( $artifact['slug'] ) ? (string) $artifact['slug'] : '';

		if ( $php === '' || $js === '' || $css === '' || $slug === '' ) {
			return null;
		}

		$generated = array(
			'php'  => $php,
			'js'   => $js,
			'css'  => $css,
			'slug' => $slug,
		);

		if ( isset( $artifact['dependencies'] ) && is_array( $artifact['dependencies'] ) ) {
			$generated['dependencies'] = $artifact['dependencies'];
		}

		return $generated;
	}

	/**
	 * @param array<string, mixed> $state
	 */
	private function loadSeedLedger( array $state ): ?RetryLedger {
		$snapshot = $state['ledger_snapshot'] ?? null;
		if ( ! is_array( $snapshot ) || $snapshot === array() ) {
			return null;
		}

		return RetryLedger::fromSnapshot( $snapshot );
	}

	/**
	 * @param array{php: string, js: string, css: string, slug: string, dependencies?: array<int, string>} $generated
	 * @param array<string, mixed>                                                                          $review
	 */
	private function failWithGenerationContext(
		string $jobId,
		array $generated,
		array $review,
		?RetryLedger $ledger,
		int $libraryVersion
	): void {
		$context = array(
			'last_artifact'   => self::buildArtifactFromGenerated( $generated ),
			'library_version' => max( 1, $libraryVersion ),
		);

		if ( $ledger !== null ) {
			$context['ledger_snapshot'] = $ledger->toSnapshot();
		} elseif ( ! empty( $review['safety_findings'] ) && is_array( $review['safety_findings'] ) ) {
			$fallbackLedger = new RetryLedger();
			$fallbackLedger->addFromReview( $review, 0 );
			$context['ledger_snapshot'] = $fallbackLedger->toSnapshot();
		}

		$this->jobManager->fail(
			$jobId,
			PluginBuilderHandler::qualityGateFailMessage( $review ),
			$context
		);
	}

	/**
	 * @param array{php: string, js: string, css: string, slug: string, dependencies?: array<int, string>} $generated
	 */
	private function trackGenerationArtifact( array $generated, int $libraryVersion ): void {
		$this->pendingFailureArtifact        = self::buildArtifactFromGenerated( $generated );
		$this->pendingFailureLibraryVersion = max( 1, $libraryVersion );
	}

	private function clearGenerationArtifact(): void {
		$this->pendingFailureArtifact       = null;
		$this->pendingFailureLibraryVersion = 1;
	}

	/**
	 * @return array<string, mixed>
	 */
	private function buildPendingFailureContext(): array {
		if ( $this->pendingFailureArtifact === null ) {
			return array();
		}

		return array(
			'last_artifact'   => $this->pendingFailureArtifact,
			'library_version' => $this->pendingFailureLibraryVersion,
		);
	}

	private function failJob( string $jobId, string $error ): void {
		$this->jobManager->fail( $jobId, $error, $this->buildPendingFailureContext() );
	}

	/**
	 * @param array{php?: string, js?: string, css?: string, slug?: string, dependencies?: array<int, string>} $generated
	 * @return array<string, mixed>
	 */
	private static function buildArtifactFromGenerated( array $generated ): array {
		$artifact = array(
			'php'  => (string) ( $generated['php'] ?? '' ),
			'js'   => (string) ( $generated['js'] ?? '' ),
			'css'  => (string) ( $generated['css'] ?? '' ),
			'slug' => (string) ( $generated['slug'] ?? '' ),
		);

		if ( isset( $generated['dependencies'] ) && is_array( $generated['dependencies'] ) ) {
			$artifact['dependencies'] = $generated['dependencies'];
		}

		return $artifact;
	}

	/**
	 * @param array<string, mixed> $code
	 * @return array<string, mixed>
	 */
	private function runReviewWithProgress( string $jobId, array $code ): array {
		return $this->reviewer->review(
			$code,
			function ( int $retryAttempt ) use ( $jobId ): void {
				$progressMessage = $retryAttempt > 1
					? 'Running another review pass…'
					: 'Running a second review pass…';
				$this->jobManager->updateProgress( $jobId, 65, $progressMessage );
			}
		);
	}

	private function deriveTitleFromCode( string $php, string $slug ): string {
		if ( preg_match( '/Plugin\s+Name:\s*(.+)/i', $php, $matches ) === 1 ) {
			$title = trim( $matches[1] );
			if ( $title !== '' ) {
				return \sanitize_text_field( $title );
			}
		}

		if ( $slug !== '' ) {
			return ucwords( str_replace( array( '-', '_' ), ' ', $slug ) );
		}

		return 'Built Plugin';
	}

	/**
	 * Generate HMAC token for a job ID.
	 */
	public static function generateToken( string $jobId ): string {
		return \hash_hmac( 'sha256', $jobId, \wp_salt( 'auth' ) . self::HMAC_SALT_SUFFIX );
	}
}
