<?php

/**
 * Installs generated plugins into wp-content/plugins on the current site.
 *
 * @package LolaCore\PluginBuilder\Packaging
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\PluginBuilder\Packaging;

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

use LolaCore\Builder\Artifact\FilesMap;
use LolaCore\Builder\Packaging\TreeInstaller;
use LolaCore\Builder\Packaging\TreeWriteException;
use LolaCore\PluginBuilder\Playground\BlueprintGenerator;
use LolaCore\PluginBuilder\PostType\PluginPostType;

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

/**
 * Writes built-plugin files to disk. Activation is always a separate, explicit step.
 *
 * The filesystem mechanics live in the shared TreeInstaller (FS-S1 backbone);
 * this class owns the Plugin Builder specifics: the 3-blob library layout
 * ({slug}.php/js/css), disk normalizers, WooCommerce bootstrap, and the
 * shipped user-facing messages.
 */
class LocalInstaller {

	private BlueprintGenerator $payloadLoader;
	private TreeInstaller $treeInstaller;

	public function __construct( ?BlueprintGenerator $payloadLoader = null, ?TreeInstaller $treeInstaller = null ) {
		$this->payloadLoader = $payloadLoader ?? new BlueprintGenerator();
		$this->treeInstaller = $treeInstaller ?? new TreeInstaller();
	}

	/**
	 * Whether the plugin directory or main file already exists on disk.
	 */
	public function isAlreadyInstalled( int $postId ): bool {
		$paths = $this->resolvePaths($postId);

		return is_dir($paths['directory']) || file_exists($paths['php_path']);
	}

	/**
	 * Install (or overwrite) a built plugin without activating it.
	 *
	 * @return array{
	 *     status: string,
	 *     post_id: int,
	 *     name: string,
	 *     slug: string,
	 *     plugin_file: string,
	 *     activated: bool,
	 *     overwritten: bool,
	 *     message: string
	 * }
	 *
	 * @throws \Exception When installation is blocked or fails.
	 */
	public function install( int $postId, bool $overwrite = false ): array {
		if (is_multisite()) {
			throw new \Exception(__('Direct installation is not available on multisite.', 'lolacore'));
		}

		$payload = $this->payloadLoader->loadPluginPayload($postId);
		$post    = get_post($postId);
		if (! $post instanceof \WP_Post) {
			throw new \Exception(__('Built plugin not found.', 'lolacore'));
		}

		$slug        = sanitize_title((string) $payload['slug']);
		$paths       = $this->resolvePathsFromSlug($slug);
		$overwritten = $this->isAlreadyInstalled($postId);

		if ($overwritten && ! $overwrite) {
			throw new \Exception(__('A version is already installed. Overwrite it?', 'lolacore'));
		}

		$this->loadAdminDependencies();

		// Enforce the asset-name and header-version contracts deterministically as
		// the code lands on disk (the library meta stays as the model produced it).
		$version = max(1, (int) get_post_meta($postId, PluginPostType::META_VERSION, true));
		$php     = PluginFileNormalizer::normalizePhp((string) $payload['php'], $slug, $version);

		$this->writePluginFiles(
			$slug,
			$php,
			(string) $payload['js'],
			(string) $payload['css']
		);

		$pluginFile = $paths['plugin_file'];

		return array(
			'status'      => 'installed',
			'post_id'     => $postId,
			'name'        => $post->post_title,
			'slug'        => $slug,
			'plugin_file' => $pluginFile,
			'activated'   => is_plugin_active($pluginFile),
			'overwritten' => $overwritten,
			'message'     => $overwritten
				? __('I updated the plugin files on this site. Activate it from Plugins when you are ready.', 'lolacore')
				: __('I installed the plugin files on this site. Activate it from Plugins when you are ready.', 'lolacore'),
		);
	}

	/**
	 * Activate a plugin that was previously installed via install().
	 *
	 * @return array{
	 *     status: string,
	 *     post_id: int,
	 *     plugin_file: string,
	 *     activated: bool,
	 *     message: string
	 * }
	 *
	 * @throws \Exception When activation is blocked or fails.
	 */
	public function activateInstalled( int $postId ): array {
		if (is_multisite()) {
			throw new \Exception(__('Direct activation is not available on multisite.', 'lolacore'));
		}

		if (! $this->isAlreadyInstalled($postId)) {
			throw new \Exception(__('Install the plugin on this site before activating it.', 'lolacore'));
		}

		$this->loadAdminDependencies();

		$paths      = $this->resolvePaths($postId);
		$pluginFile = $paths['plugin_file'];

		if (! file_exists($paths['php_path'])) {
			throw new \Exception(__('The plugin main file is missing. Try installing again.', 'lolacore'));
		}

		if (is_plugin_active($pluginFile)) {
			return array(
				'status'      => 'activated',
				'post_id'     => $postId,
				'plugin_file' => $pluginFile,
				'activated'   => true,
				'message'     => __('This plugin is already active.', 'lolacore'),
			);
		}

		$php = $this->syncNormalizedPluginFile($postId, $paths['php_path']);
		self::bootstrapWooCommerceActivation($php);

		$activation = activate_plugin($pluginFile);
		if (is_wp_error($activation)) {
			throw new \Exception(
				esc_html(
					sprintf(
						/* translators: %s: WordPress activation error message */
						__('I could not activate the plugin: %s', 'lolacore'),
						$activation->get_error_message()
					)
				)
			);
		}

		return array(
			'status'      => 'activated',
			'post_id'     => $postId,
			'plugin_file' => $pluginFile,
			'activated'   => true,
			'message'     => __('The plugin is now active.', 'lolacore'),
		);
	}

	/**
	 * Read the plugin files currently on disk for a built plugin.
	 *
	 * Read-only introspection of what is actually live on the site — which may
	 * have drifted from the library if the user iterated without reinstalling.
	 * Lets the agent verify the live version instead of inferring it from behavior.
	 *
	 * @return array{installed: bool, active: bool, plugin_file: string, version_header: string, php: string, js: string, css: string}
	 *
	 * @throws \Exception When the built plugin slug is missing.
	 */
	public function readInstalled( int $postId ): array {
		$paths      = $this->resolvePaths($postId);
		$pluginFile = $paths['plugin_file'];
		$installed  = is_dir($paths['directory']) || file_exists($paths['php_path']);

		if (! $installed) {
			return array(
				'installed'      => false,
				'active'         => false,
				'plugin_file'    => $pluginFile,
				'version_header' => '',
				'php'            => '',
				'js'             => '',
				'css'            => '',
			);
		}

		$this->loadAdminDependencies();

		$php = $this->readFileIfExists($paths['php_path']);

		return array(
			'installed'      => true,
			'active'         => function_exists('is_plugin_active') && is_plugin_active($pluginFile),
			'plugin_file'    => $pluginFile,
			'version_header' => self::parseVersionHeader($php),
			'php'            => $php,
			'js'             => $this->readFileIfExists($paths['js_path']),
			'css'            => $this->readFileIfExists($paths['css_path']),
		);
	}

	/**
	 * Extract the `Version:` value from a plugin header, or '' when absent.
	 */
	public static function parseVersionHeader( string $php ): string {
		if (preg_match('/^\s*\*\s*Version:\s*(.+)$/mi', $php, $matches) === 1) {
			return trim($matches[1]);
		}

		return '';
	}

	/**
	 * Read a file from disk via the WordPress filesystem, or '' if unavailable.
	 */
	private function readFileIfExists( string $path ): string {
		if (! file_exists($path)) {
			return '';
		}

		$this->initFilesystem();

		global $wp_filesystem;
		if ($wp_filesystem instanceof \WP_Filesystem_Base) {
			$contents = $wp_filesystem->get_contents($path);

			return is_string($contents) ? $contents : '';
		}

		return '';
	}

	/**
	 * @return array{directory: string, php_path: string, js_path: string, css_path: string, plugin_file: string}
	 */
	private function resolvePaths( int $postId ): array {
		$slug = (string) get_post_meta($postId, PluginPostType::META_SLUG, true);
		if ($slug === '') {
			throw new \Exception(__('Built plugin slug is missing.', 'lolacore'));
		}

		return $this->resolvePathsFromSlug($slug);
	}

	/**
	 * @return array{directory: string, php_path: string, js_path: string, css_path: string, plugin_file: string}
	 */
	private function resolvePathsFromSlug( string $slug ): array {
		$directory = WP_PLUGIN_DIR . '/' . $slug;

		return array(
			'directory'   => $directory,
			'php_path'    => $directory . '/' . $slug . '.php',
			'js_path'     => $directory . '/' . $slug . '.js',
			'css_path'    => $directory . '/' . $slug . '.css',
			'plugin_file' => $slug . '/' . $slug . '.php',
		);
	}

	/**
	 * Write the 3-blob library layout through the shared TreeInstaller,
	 * mapping tree failures to the shipped per-file messages.
	 */
	private function writePluginFiles( string $slug, string $php, string $js, string $css ): void {
		try {
			$this->treeInstaller->writeTree(
				$slug,
				FilesMap::fromArray(array(
					$slug . '.php' => $php,
					$slug . '.js'  => $js,
					$slug . '.css' => $css,
				))
			);
		} catch (TreeWriteException $e) {
			throw new \Exception( esc_html( $this->treeFailureMessage($e->relPath()) ) );
		}
	}

	private function treeFailureMessage( string $relPath ): string {
		return match (strtolower(pathinfo($relPath, PATHINFO_EXTENSION))) {
			'php'   => __('Could not write the plugin PHP file.', 'lolacore'),
			'js'    => __('Could not write the plugin JavaScript file.', 'lolacore'),
			'css'   => __('Could not write the plugin CSS file.', 'lolacore'),
			default => __('Could not create the plugin directory.', 'lolacore'),
		};
	}

	/**
	 * Re-apply disk normalizers to an installed plugin file (idempotent).
	 *
	 * Repairs legacy installs that were written before a normalizer existed — e.g.
	 * WooCommerce settings subclasses missing the WC_Settings_Page require.
	 */
	private function syncNormalizedPluginFile( int $postId, string $phpPath ): string {
		if (! is_readable($phpPath)) {
			return '';
		}

		$php = file_get_contents($phpPath);
		if ($php === false || $php === '') {
			return '';
		}

		$slug    = sanitize_title((string) get_post_meta($postId, PluginPostType::META_SLUG, true));
		$version = max(1, (int) get_post_meta($postId, PluginPostType::META_VERSION, true));
		$normalized = PluginFileNormalizer::normalizePhp($php, $slug, $version);

		if ($normalized !== $php) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Mirrors writePluginFiles; WP_Filesystem not required for a single-file patch.
			file_put_contents($phpPath, $normalized);
		}

		return $normalized;
	}

	/**
	 * Load WooCommerce admin bases referenced by a plugin before activate_plugin() includes it.
	 *
	 * @throws \Exception When WooCommerce is required but inactive.
	 */
	public static function bootstrapWooCommerceActivation( string $php ): void {
		if (! str_contains($php, 'WC_Settings_Page')) {
			return;
		}

		if (! class_exists('WooCommerce', false)) {
			throw new \Exception(__('WooCommerce must be active before I can activate this plugin.', 'lolacore'));
		}

		if (class_exists('WC_Settings_Page', false) || ! defined('WC_ABSPATH')) {
			return;
		}

		require_once WC_ABSPATH . 'includes/admin/settings/class-wc-settings-page.php';
	}

	private function loadAdminDependencies(): void {
		if (! function_exists('WP_Filesystem')) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}
		if (! function_exists('activate_plugin')) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
	}

	private function initFilesystem(): void {
		global $wp_filesystem;

		if ($wp_filesystem instanceof \WP_Filesystem_Base) {
			return;
		}

		if (! WP_Filesystem()) {
			throw new \Exception(__('Could not initialize the WordPress filesystem.', 'lolacore'));
		}
	}
}

