<?php

/**
 * Playground-first attestation gate before installing built artifacts on the live site.
 *
 * @package LolaCore\Builder\Gate
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\Builder\Gate;

use LolaCore\Builder\Artifact\ArtifactAdapter;
use LolaCore\Builder\Runtime\RuntimeValidator;
use LolaCore\I18n\ErrorMessages;

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

/**
 * Tracks Playground testing attestation per artifact version and blocks local
 * install until attested.
 *
 * Meta keys and load-safe transient names are derived from the adapter's meta
 * prefix, reproducing the Plugin Builder's shipped keys exactly for the
 * '_lc_pb_' prefix ('_lc_pb_pg_gate_status', 'lc_pb_loadsafe_{id}_{v}', …).
 * Those derivations are a compatibility contract: live libraries carry them.
 */
final class AttestationGate {

	public const STATUS_UNTESTED = 'untested';
	public const STATUS_OPENED   = 'opened';
	public const STATUS_ATTESTED = 'attested';
	public const STATUS_FAILED   = 'failed';

	private const LOADSAFE_CACHE_TTL = 86400;

	public function __construct( private ArtifactAdapter $adapter ) {
	}

	public function metaStatusKey(): string {
		return $this->adapter->metaPrefix() . 'pg_gate_status';
	}

	public function metaAttestedVersionKey(): string {
		return $this->adapter->metaPrefix() . 'pg_attested_version';
	}

	public function metaAttestedAtKey(): string {
		return $this->adapter->metaPrefix() . 'pg_attested_at';
	}

	public function metaOpenedAtKey(): string {
		return $this->adapter->metaPrefix() . 'pg_opened_at';
	}

	/**
	 * Reset gate when a new library version is saved (create or update).
	 */
	public function reset( int $post_id ): void {
		$this->invalidateLoadSafeCache($post_id);
		update_post_meta($post_id, $this->metaStatusKey(), self::STATUS_UNTESTED);
		delete_post_meta($post_id, $this->metaAttestedVersionKey());
		delete_post_meta($post_id, $this->metaAttestedAtKey());
		delete_post_meta($post_id, $this->metaOpenedAtKey());
	}

	/**
	 * Record that the user opened Playground (flow checkpoint, not technical verification).
	 */
	public function markOpened( int $post_id ): void {
		$status = $this->getStatus($post_id);
		if ($status === self::STATUS_ATTESTED && $this->isInstallAllowed($post_id)) {
			return;
		}

		update_post_meta($post_id, $this->metaStatusKey(), self::STATUS_OPENED);
		update_post_meta($post_id, $this->metaOpenedAtKey(), gmdate('Y-m-d H:i:s'));
	}

	/**
	 * User confirmed Playground testing succeeded for the current version.
	 */
	public function markAttested( int $post_id ): void {
		$version = $this->adapter->currentVersion($post_id);
		update_post_meta($post_id, $this->metaStatusKey(), self::STATUS_ATTESTED);
		update_post_meta($post_id, $this->metaAttestedVersionKey(), $version);
		update_post_meta($post_id, $this->metaAttestedAtKey(), gmdate('Y-m-d H:i:s'));
	}

	/**
	 * User reported issues during Playground testing.
	 */
	public function markFailed( int $post_id ): void {
		update_post_meta($post_id, $this->metaStatusKey(), self::STATUS_FAILED);
		delete_post_meta($post_id, $this->metaAttestedVersionKey());
		delete_post_meta($post_id, $this->metaAttestedAtKey());
	}

	public function getStatus( int $post_id ): string {
		$status  = (string) get_post_meta($post_id, $this->metaStatusKey(), true);
		$allowed = array(
			self::STATUS_UNTESTED,
			self::STATUS_OPENED,
			self::STATUS_ATTESTED,
			self::STATUS_FAILED,
		);

		return in_array($status, $allowed, true) ? $status : self::STATUS_UNTESTED;
	}

	public function getAttestedVersion( int $post_id ): int {
		return max(0, (int) get_post_meta($post_id, $this->metaAttestedVersionKey(), true));
	}

	public function isInstallAllowed( int $post_id ): bool {
		if ($this->getStatus($post_id) !== self::STATUS_ATTESTED) {
			return false;
		}

		return $this->getAttestedVersion($post_id) === $this->adapter->currentVersion($post_id);
	}

	/**
	 * @throws \Exception When install is blocked by the Playground gate.
	 */
	public function assertInstallAllowed( int $post_id ): void {
		if ($this->isInstallAllowed($post_id)) {
			return;
		}

		throw new \Exception( esc_html( ErrorMessages::installBlockedPlayground() ) );
	}

	/**
	 * Load-time fatal check before offering Playground or installing locally.
	 *
	 * @throws \Exception When generated PHP fatals on load.
	 */
	public function assertLoadSafe( int $post_id, ?RuntimeValidator $validator = null ): void {
		$php = $this->adapter->storedPhp($post_id);
		if ($php === '') {
			return;
		}

		$version  = $this->adapter->currentVersion($post_id);
		$php_hash = md5($php);
		$cached   = get_transient($this->loadSafeCacheKey($post_id, $version));

		if (is_array($cached) && ($cached['php_hash'] ?? '') === $php_hash) {
			if (($cached['status'] ?? '') === RuntimeValidator::STATUS_FATAL) {
				if (RuntimeValidator::isStaleMisclassifiedFatalMessage((string) ( $cached['message'] ?? '' ))) {
					delete_transient($this->loadSafeCacheKey($post_id, $version));
				} else {
					$this->throwLoadFatal(
						(string) ( $cached['message'] ?? self::defaultLoadFatalMessage() ),
						(int) ( $cached['line'] ?? 0 )
					);
				}
			} else {
				return;
			}
		}

		$slug         = $this->adapter->slug($post_id);
		$dependencies = $this->adapter->dependencies($post_id);

		$phpForRuntime = $this->adapter->runtimePhp($post_id);

		$checker = $validator ?? new RuntimeValidator();
		$result  = $checker->validate($phpForRuntime, $slug !== '' ? $slug : 'plugin', $dependencies);

		$this->storeLoadSafeCache($post_id, $version, $php, $result);

		if (($result['status'] ?? '') !== RuntimeValidator::STATUS_FATAL) {
			return;
		}

		$this->throwLoadFatal(
			isset($result['message']) ? (string) $result['message'] : self::defaultLoadFatalMessage(),
			isset($result['line']) ? (int) $result['line'] : 0
		);
	}

	private static function defaultLoadFatalMessage(): string {
		return __('The plugin failed to load.', 'lolacore');
	}

	/**
	 * Seed load-safe cache from build-time validation so later gate checks do
	 * not repeat the same RuntimeValidator loopback scrape.
	 *
	 * @param array{status?: string, message?: string, line?: int} $runtime_result
	 */
	public function seedLoadSafeCache( int $post_id, int $version, string $php, array $runtime_result ): void {
		$this->storeLoadSafeCache($post_id, $version, $php, $this->sanitizeRuntimeResult($runtime_result));
	}

	/**
	 * Gate flags exposed to chat artifacts and admin UI.
	 *
	 * @return array{gate_status: string, install_allowed: bool, attested_version: int, current_version: int}
	 */
	public function uiFlags( int $post_id ): array {
		return array(
			'gate_status'      => $this->getStatus($post_id),
			'install_allowed'  => $this->isInstallAllowed($post_id),
			'attested_version' => $this->getAttestedVersion($post_id),
			'current_version'  => $this->adapter->currentVersion($post_id),
		);
	}

	private function loadSafeCacheKey( int $post_id, int $version ): string {
		return trim($this->adapter->metaPrefix(), '_') . '_loadsafe_' . $post_id . '_' . $version;
	}

	private function invalidateLoadSafeCache( int $post_id ): void {
		delete_transient($this->loadSafeCacheKey($post_id, $this->adapter->currentVersion($post_id)));
	}

	/**
	 * @param array{status?: string, message?: string, line?: int} $result
	 * @return array{php_hash: string, status: string, message: string, line: int}
	 */
	private function loadSafeCachePayload( string $php, array $result ): array {
		return array(
			'php_hash' => md5($php),
			'status'   => (string) ( $result['status'] ?? RuntimeValidator::STATUS_SKIPPED ),
			'message'  => (string) ( $result['message'] ?? '' ),
			'line'     => (int) ( $result['line'] ?? 0 ),
		);
	}

	/**
	 * @param array{status?: string, message?: string, line?: int} $result
	 */
	private function storeLoadSafeCache( int $post_id, int $version, string $php, array $result ): void {
		set_transient(
			$this->loadSafeCacheKey($post_id, $version),
			$this->loadSafeCachePayload($php, $this->sanitizeRuntimeResult($result)),
			self::LOADSAFE_CACHE_TTL
		);
	}

	/**
	 * Never persist infra scrape noise as a blocking fatal (no line, generic message).
	 *
	 * @param array{status?: string, message?: string, line?: int} $result
	 * @return array{status?: string, message?: string, line?: int}
	 */
	private function sanitizeRuntimeResult( array $result ): array {
		$status  = (string) ( $result['status'] ?? RuntimeValidator::STATUS_SKIPPED );
		$message = (string) ( $result['message'] ?? '' );
		$line    = (int) ( $result['line'] ?? 0 );

		if ($status === RuntimeValidator::STATUS_FATAL) {
			if ($line <= 0 || RuntimeValidator::isStaleMisclassifiedFatalMessage($message)) {
				return array(
					'status'  => RuntimeValidator::STATUS_SKIPPED,
					'message' => '',
					'line'    => 0,
				);
			}
		}

		return array(
			'status'  => $status,
			'message' => $message,
			'line'    => $line,
		);
	}

	/**
	 * @throws \Exception When generated PHP fatals on load.
	 */
	private function throwLoadFatal( string $message, int $line ): void {
		throw new \Exception(
			esc_html(
				sprintf(
					/* translators: 1: optional line number suffix, 2: PHP fatal error message */
					__('This plugin triggers a fatal error when WordPress loads it%1$s: %2$s Ask me to fix it before testing or installing.', 'lolacore'),
					$line > 0
						? sprintf(
							/* translators: %d: line number in PHP source */
							__(' (line %d)', 'lolacore'),
							$line
						)
						: '',
					$message
				)
			)
		);
	}
}
